From 445467167bc7dc89e65974469ccb296eeb71dbbb Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Mon, 2 Nov 2020 12:25:36 -0500 Subject: [PATCH 01/18] Implement Application Factory pattern --- AIPscan/Aggregator/__init__.py | 7 ------- AIPscan/Aggregator/tasks.py | 3 ++- AIPscan/Aggregator/views.py | 21 ++++++++++----------- AIPscan/__init__.py | 27 +-------------------------- AIPscan/application.py | 27 +++++++++++++++++++++++++++ AIPscan/celery.py | 24 ++++++++++++++++++++++++ AIPscan/extensions.py | 14 ++++++++++++++ AIPscan/worker.py | 5 +++++ README.md | 2 +- celery_config.py | 8 ++++++++ config.py | 11 ----------- create_aipscan.db.py | 3 +++ flask_celery.py | 24 ------------------------ run.py | 4 +++- 14 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 AIPscan/application.py create mode 100644 AIPscan/celery.py create mode 100644 AIPscan/extensions.py create mode 100644 AIPscan/worker.py create mode 100644 celery_config.py delete mode 100644 flask_celery.py diff --git a/AIPscan/Aggregator/__init__.py b/AIPscan/Aggregator/__init__.py index f94f2d91..e69de29b 100644 --- a/AIPscan/Aggregator/__init__.py +++ b/AIPscan/Aggregator/__init__.py @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from AIPscan import db - -# Setup and create database if it doesn't exist. If it does exist, the -# create_all() function will only create the tables which don't exist. -db.create_all() diff --git a/AIPscan/Aggregator/tasks.py b/AIPscan/Aggregator/tasks.py index db100e0e..18483a5c 100644 --- a/AIPscan/Aggregator/tasks.py +++ b/AIPscan/Aggregator/tasks.py @@ -6,8 +6,9 @@ from celery.utils.log import get_task_logger -from AIPscan import celery +# from AIPscan.extensions import celery from AIPscan import db +from AIPscan.extensions import celery from AIPscan.models import ( FetchJob, # Custom celery Models. diff --git a/AIPscan/Aggregator/views.py b/AIPscan/Aggregator/views.py index db608c01..5ed8a6b8 100644 --- a/AIPscan/Aggregator/views.py +++ b/AIPscan/Aggregator/views.py @@ -1,8 +1,17 @@ # -*- coding: utf-8 -*- +from datetime import datetime +import os +import shutil + +from celery.result import AsyncResult from flask import Blueprint, render_template, redirect, request, flash, url_for, jsonify -from AIPscan import db, app, celery +from AIPscan import db +from AIPscan.Aggregator.task_helpers import get_packages_directory +from AIPscan.Aggregator.forms import StorageServiceForm +from AIPscan.Aggregator import tasks +from AIPscan.extensions import celery from AIPscan.models import ( FetchJob, StorageService, @@ -11,15 +20,6 @@ get_mets_tasks, ) -from AIPscan.Aggregator.task_helpers import get_packages_directory - -from AIPscan.Aggregator.forms import StorageServiceForm -from AIPscan.Aggregator import tasks -import os -import shutil -from datetime import datetime -from celery.result import AsyncResult - aggregator = Blueprint("aggregator", __name__, template_folder="templates") @@ -39,7 +39,6 @@ def _format_date(date_string): return formatted_date.strftime(DATE_FORMAT_PARTIAL) -@app.route("/") @aggregator.route("/", methods=["GET"]) def ss_default(): # load the default storage service diff --git a/AIPscan/__init__.py b/AIPscan/__init__.py index fc8e7e96..6aeb4916 100644 --- a/AIPscan/__init__.py +++ b/AIPscan/__init__.py @@ -1,30 +1,5 @@ # -*- coding: utf-8 -*- -from flask import Flask from flask_sqlalchemy import SQLAlchemy -app = Flask(__name__) -app.config.from_object("config") -db = SQLAlchemy(app) - -from celery import Celery -from flask_celery import make_celery - -# PICTURAE TODO: Create a different app configuration for celery. If -# we inspect the celery object below celery.__dict__ we can see all -# of the app consts have been consumed by the celery constructor, -# probably as a **kwarg and hasn't decided to rid itself of any values -# that are superfluous. -celery = make_celery(app) - -from AIPscan import models - -from AIPscan.Aggregator.views import aggregator -from AIPscan.Reporter.views import reporter -from AIPscan.User.views import user -from AIPscan.API.views import api - -app.register_blueprint(aggregator, url_prefix="/aggregator") -app.register_blueprint(reporter, url_prefix="/reporter") -app.register_blueprint(user, url_prefix="/user") -app.register_blueprint(api) +db = SQLAlchemy() diff --git a/AIPscan/application.py b/AIPscan/application.py new file mode 100644 index 00000000..ae619bde --- /dev/null +++ b/AIPscan/application.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from flask import Flask + +from AIPscan.Aggregator.views import aggregator +from AIPscan.Reporter.views import reporter +from AIPscan.User.views import user +from AIPscan.API.views import api + +from AIPscan import db +from AIPscan.celery import configure_celery + + +def create_app(): + """Flask app factory, returns app instance.""" + app = Flask(__name__) + app.config.from_object("config") + + app.register_blueprint(aggregator, url_prefix="/aggregator") + app.register_blueprint(reporter, url_prefix="/reporter") + app.register_blueprint(user, url_prefix="/user") + app.register_blueprint(api) + + db.init_app(app) + configure_celery(app) + + return app diff --git a/AIPscan/celery.py b/AIPscan/celery.py new file mode 100644 index 00000000..2472c5a1 --- /dev/null +++ b/AIPscan/celery.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from AIPscan import extensions + +# PICTURAE TODO: Create a different app configuration for celery. If +# we inspect the celery object below celery.__dict__ we can see all +# of the app consts have been consumed by the celery constructor, +# probably as a **kwarg and hasn't decided to rid itself of any values +# that are superfluous. + + +def configure_celery(app): + """Add Flask app context to celery.Task.""" + TaskBase = extensions.celery.Task + + class ContextTask(TaskBase): + abstract = True + + def __call__(self, *args, **kwargs): + with app.app_context(): + return TaskBase.__call__(self, *args, **kwargs) + + extensions.celery.Task = ContextTask + return extensions.celery diff --git a/AIPscan/extensions.py b/AIPscan/extensions.py new file mode 100644 index 00000000..36de9b0d --- /dev/null +++ b/AIPscan/extensions.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from celery import Celery + +from celery_config import CELERY_RESULT_BACKEND, CELERY_BROKER_URL + +# Celery instance that will be initialized at import time and then +# further configured via AIPscan.celery's configure_celery method. +celery = Celery( + "tasks", + backend=CELERY_RESULT_BACKEND, + broker=CELERY_BROKER_URL, + include=["AIPscan.Aggregator.tasks"], +) diff --git a/AIPscan/worker.py b/AIPscan/worker.py new file mode 100644 index 00000000..5db56712 --- /dev/null +++ b/AIPscan/worker.py @@ -0,0 +1,5 @@ +from AIPscan.application import create_app +from AIPscan.celery import configure_celery + +app = create_app() +celery = configure_celery(app) diff --git a/README.md b/README.md index 5c251588..93efe474 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ and AIPScan will automatically be able to connect to the queue at `:5672`. ### Celery * In another terminal window, from the AIPscan root directory, start a Celery -worker: `celery -A AIPscan.Aggregator.tasks worker --loglevel=info` +worker: `celery worker -A AIPscan.worker.celery --loglevel=info` ## Usage diff --git a/celery_config.py b/celery_config.py new file mode 100644 index 00000000..a0b90b2d --- /dev/null +++ b/celery_config.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +import os + +CELERY_RESULT_BACKEND = os.getenv( + "CELERY_RESULT_BACKEND", "db+sqlite:///celerytasks.db" +) +CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "amqp://guest@localhost//") diff --git a/config.py b/config.py index 98b0e354..af0713f4 100644 --- a/config.py +++ b/config.py @@ -8,17 +8,6 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(basedir, "aipscan.db") SQLALCHEMY_ECHO = False -CELERY_BROKER_URL = "amqp://guest@localhost//" - -"""PICTURAR TODO: - -We get different protocol errors for these connection strings depending -on which we use and where. - - * SQLAlchemy: Can't load plugin: sqlalchemy.dialects:db.sqlite ("db+sqlite://") - * Celery: No module named sqlite ("sqlite://") -""" -CELERY_RESULT_BACKEND = "db+sqlite:///celerytasks.db" SQLALCHEMY_CELERY_BACKEND = "sqlite:///" + os.path.join(basedir, "celerytasks.db") # change to a long random code (e.g. UUID) when pushing to production diff --git a/create_aipscan.db.py b/create_aipscan.db.py index abfb46ec..5422b00f 100755 --- a/create_aipscan.db.py +++ b/create_aipscan.db.py @@ -4,5 +4,8 @@ # ./create_aipscan.db.py from AIPscan import db +from AIPscan.application import create_app +app = create_app() +app.app_context().push() db.create_all() diff --git a/flask_celery.py b/flask_celery.py deleted file mode 100644 index 8ce8ab11..00000000 --- a/flask_celery.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- - -from celery import Celery - - -def make_celery(app): - task_queue = Celery( - app.import_name, - backend=app.config["CELERY_RESULT_BACKEND"], - broker=app.config["CELERY_BROKER_URL"], - ) - task_queue.conf.update(app.config) - - TaskBase = task_queue.Task - - class ContextTask(TaskBase): - abstract = True - - def __call__(self, *args, **kwargs): - with app.app_context(): - return TaskBase.__call__(self, *args, **kwargs) - - task_queue.Task = ContextTask - return task_queue diff --git a/run.py b/run.py index 3040efdd..b35adb7c 100644 --- a/run.py +++ b/run.py @@ -1,4 +1,6 @@ -from AIPscan import app +from AIPscan.application import create_app + if __name__ == "__main__": + app = create_app() app.run(debug=True, host="0.0.0.0") From aa398c8f6f3d618ebd8a93a8902664d98f4fcf9f Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Mon, 2 Nov 2020 14:05:39 -0500 Subject: [PATCH 02/18] Add Home blueprint to handle root route --- AIPscan/Home/__init__.py | 0 AIPscan/Home/views.py | 11 +++++++++++ AIPscan/application.py | 2 ++ 3 files changed, 13 insertions(+) create mode 100644 AIPscan/Home/__init__.py create mode 100644 AIPscan/Home/views.py diff --git a/AIPscan/Home/__init__.py b/AIPscan/Home/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/AIPscan/Home/views.py b/AIPscan/Home/views.py new file mode 100644 index 00000000..5bf8b189 --- /dev/null +++ b/AIPscan/Home/views.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +from flask import Blueprint, redirect, url_for + +home = Blueprint("home", __name__) + + +@home.route("/", methods=["GET"]) +def index(): + """Define handling for application's / route.""" + return redirect(url_for("aggregator.ss_default")) diff --git a/AIPscan/application.py b/AIPscan/application.py index ae619bde..9a05dc95 100644 --- a/AIPscan/application.py +++ b/AIPscan/application.py @@ -6,6 +6,7 @@ from AIPscan.Reporter.views import reporter from AIPscan.User.views import user from AIPscan.API.views import api +from AIPscan.Home.views import home from AIPscan import db from AIPscan.celery import configure_celery @@ -20,6 +21,7 @@ def create_app(): app.register_blueprint(reporter, url_prefix="/reporter") app.register_blueprint(user, url_prefix="/user") app.register_blueprint(api) + app.register_blueprint(home) db.init_app(app) configure_celery(app) From f9e1658a9a97b8e913599878fbe257f7a369a7f9 Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Mon, 2 Nov 2020 15:08:47 -0500 Subject: [PATCH 03/18] Enable multiple app configs --- AIPscan/application.py | 6 ++++-- config.py | 34 +++++++++++++++++++++++++++------- run.py | 7 +++++-- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/AIPscan/application.py b/AIPscan/application.py index 9a05dc95..1a10447c 100644 --- a/AIPscan/application.py +++ b/AIPscan/application.py @@ -10,12 +10,14 @@ from AIPscan import db from AIPscan.celery import configure_celery +from config import CONFIGS -def create_app(): +def create_app(config_name="default"): """Flask app factory, returns app instance.""" app = Flask(__name__) - app.config.from_object("config") + + app.config.from_object(CONFIGS[config_name]) app.register_blueprint(aggregator, url_prefix="/aggregator") app.register_blueprint(reporter, url_prefix="/reporter") diff --git a/config.py b/config.py index af0713f4..ea3afaee 100644 --- a/config.py +++ b/config.py @@ -4,13 +4,33 @@ basedir = os.path.abspath(os.path.dirname(__file__)) -# change to os.environ settings in production -SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(basedir, "aipscan.db") -SQLALCHEMY_ECHO = False +DEFAULT_AIPSCAN_DB = "sqlite:///" + os.path.join(basedir, "aipscan.db") +DEFAULT_CELERY_DB = "sqlite:///" + os.path.join(basedir, "celerytasks.db") -SQLALCHEMY_CELERY_BACKEND = "sqlite:///" + os.path.join(basedir, "celerytasks.db") -# change to a long random code (e.g. UUID) when pushing to production -SECRET_KEY = os.environ.get("SECRET_KEY") or "you-will-never-guess" +class Config: + # Be sure to set a secure secret key for production. + SECRET_KEY = os.getenv("SECRET_KEY", "you-will-never-guess") -SQLALCHEMY_BINDS = {"celery": SQLALCHEMY_CELERY_BACKEND} + DEBUG = False + TESTING = False + + SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI", DEFAULT_AIPSCAN_DB) + SQLALCHEMY_CELERY_BACKEND = os.getenv( + "SQLALCHEMY_CELERY_BACKEND", DEFAULT_CELERY_DB + ) + SQLALCHEMY_BINDS = {"celery": SQLALCHEMY_CELERY_BACKEND} + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_ECHO = False + + +class DevelopmentConfig(Config): + DEBUG = True + + +class TestConfig(Config): + TESTING = True + SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" + + +CONFIGS = {"dev": DevelopmentConfig, "test": TestConfig, "default": Config} diff --git a/run.py b/run.py index b35adb7c..5fc20f93 100644 --- a/run.py +++ b/run.py @@ -1,6 +1,9 @@ +import os + from AIPscan.application import create_app if __name__ == "__main__": - app = create_app() - app.run(debug=True, host="0.0.0.0") + config_name = os.environ.get("FLASK_CONFIG", "default") + app = create_app(config_name) + app.run(host="0.0.0.0") From 0a4d43f9ac9761f6e8484c60e895c35dcb10d755 Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Mon, 2 Nov 2020 15:50:21 -0500 Subject: [PATCH 04/18] Add app_instance fixtures and example db test This commit adds module-wide `app_instance` fixtures which are used to provide application context to tests under the Application Factory pattern, as well as a test that demonstrates how to test database state using our test config with in-memory test database. Due to how pytest discovers fixtures, removing the duplication of the fixtures as defined in conftest modules would require re-organizing our tests into a single AIPscan.tests directory. --- AIPscan/Aggregator/tasks.py | 6 +-- AIPscan/Aggregator/tests/conftest.py | 22 +++++++++++ .../Aggregator/tests/test_database_helpers.py | 38 +++++++++++++++++-- AIPscan/Data/tests/conftest.py | 22 +++++++++++ AIPscan/Data/tests/test_largest_files.py | 10 ++++- 5 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 AIPscan/Aggregator/tests/conftest.py create mode 100644 AIPscan/Data/tests/conftest.py diff --git a/AIPscan/Aggregator/tasks.py b/AIPscan/Aggregator/tasks.py index 18483a5c..441aea1a 100644 --- a/AIPscan/Aggregator/tasks.py +++ b/AIPscan/Aggregator/tasks.py @@ -16,7 +16,7 @@ ) from AIPscan.Aggregator.celery_helpers import write_celery_update -from AIPscan.Aggregator.database_helpers import create_aip_object, process_aip_data +from AIPscan.Aggregator import database_helpers from AIPscan.Aggregator.mets_parse_helpers import ( _download_mets, @@ -297,7 +297,7 @@ def get_mets( # log and act upon. original_name = package_uuid - aip = create_aip_object( + aip = database_helpers.create_aip_object( package_uuid=package_uuid, transfer_name=original_name, create_date=mets.createdate, @@ -305,4 +305,4 @@ def get_mets( fetch_job_id=fetch_job_id, ) - process_aip_data(aip, mets) + database_helpers.process_aip_data(aip, mets) diff --git a/AIPscan/Aggregator/tests/conftest.py b/AIPscan/Aggregator/tests/conftest.py new file mode 100644 index 00000000..eb30886c --- /dev/null +++ b/AIPscan/Aggregator/tests/conftest.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +import pytest + +from AIPscan import db +from AIPscan.application import create_app + + +@pytest.fixture +def app_instance(): + """Pytest fixture that returns an instance of our application. + + This fixture provides a Flask application context for tests using + AIPscan's test configuration. + + This pattern can be extended in additional fixtures to, e.g. load + state to the test database from a fixture as needed for tests. + """ + app = create_app("test") + with app.app_context(): + db.create_all() + return app diff --git a/AIPscan/Aggregator/tests/test_database_helpers.py b/AIPscan/Aggregator/tests/test_database_helpers.py index d10e534e..d7f47ce3 100644 --- a/AIPscan/Aggregator/tests/test_database_helpers.py +++ b/AIPscan/Aggregator/tests/test_database_helpers.py @@ -1,16 +1,40 @@ # -*- coding: utf-8 -*- - import os +import uuid import metsrw import pytest from AIPscan.Aggregator import database_helpers -from AIPscan.models import Agent +from AIPscan.models import Agent, AIP FIXTURES_DIR = "fixtures" +def test_create_aip(app_instance): + """Test AIP creation.""" + app_instance.app_context().push() + + PACKAGE_UUID = str(uuid.uuid4()) + TRANSFER_NAME = "some name" + STORAGE_SERVICE_ID = 1 + FETCH_JOB_ID = 1 + + database_helpers.create_aip_object( + package_uuid=PACKAGE_UUID, + transfer_name=TRANSFER_NAME, + create_date="2020-11-02", + storage_service_id=STORAGE_SERVICE_ID, + fetch_job_id=FETCH_JOB_ID, + ) + + aip = AIP.query.filter_by(uuid=PACKAGE_UUID).first() + assert aip is not None + assert aip.transfer_name == TRANSFER_NAME + assert aip.storage_service_id == STORAGE_SERVICE_ID + assert aip.fetch_job_id == FETCH_JOB_ID + + @pytest.mark.parametrize( "fixture_path, event_count, agent_link_multiplier", [ @@ -19,11 +43,15 @@ (os.path.join("images_mets", "images.xml"), 76, 3), ], ) -def test_event_creation(mocker, fixture_path, event_count, agent_link_multiplier): +def test_event_creation( + app_instance, mocker, fixture_path, event_count, agent_link_multiplier +): """Make sure that we're seeing all of the events associated with an AIP and that they are potentially written to the database okay. Make sure too that the event_agent_relationship is established. """ + app_instance.app_context().push() + script_dir = os.path.dirname(os.path.realpath(__file__)) mets_file = os.path.join(script_dir, FIXTURES_DIR, fixture_path) mets = metsrw.METSDocument.fromfile(mets_file) @@ -56,11 +84,13 @@ def test_event_creation(mocker, fixture_path, event_count, agent_link_multiplier (os.path.join("images_mets", "images.xml"), 3), ], ) -def test_collect_agents(fixture_path, number_of_unique_agents): +def test_collect_agents(app_instance, fixture_path, number_of_unique_agents): """Make sure that we retrieve only unique Agents from the METS to then add to the database. Agents are "repeated" per PREMIS:OBJECT in METS. """ + app_instance.app_context().push() + script_dir = os.path.dirname(os.path.realpath(__file__)) mets_file = os.path.join(script_dir, FIXTURES_DIR, fixture_path) mets = metsrw.METSDocument.fromfile(mets_file) diff --git a/AIPscan/Data/tests/conftest.py b/AIPscan/Data/tests/conftest.py new file mode 100644 index 00000000..eb30886c --- /dev/null +++ b/AIPscan/Data/tests/conftest.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +import pytest + +from AIPscan import db +from AIPscan.application import create_app + + +@pytest.fixture +def app_instance(): + """Pytest fixture that returns an instance of our application. + + This fixture provides a Flask application context for tests using + AIPscan's test configuration. + + This pattern can be extended in additional fixtures to, e.g. load + state to the test database from a fixture as needed for tests. + """ + app = create_app("test") + with app.app_context(): + db.create_all() + return app diff --git a/AIPscan/Data/tests/test_largest_files.py b/AIPscan/Data/tests/test_largest_files.py index 7a373333..e8319750 100644 --- a/AIPscan/Data/tests/test_largest_files.py +++ b/AIPscan/Data/tests/test_largest_files.py @@ -75,9 +75,11 @@ @pytest.mark.parametrize( "file_data, file_count", [([], 0), (TEST_FILES, 3), (TEST_FILES[:2], 2)] ) -def test_largest_files(mocker, file_data, file_count): +def test_largest_files(app_instance, mocker, file_data, file_count): """Test that return value conforms to expected structure. """ + app_instance.app_context().push() + mock_query = mocker.patch("AIPscan.Data.data._largest_files_query") mock_query.return_value = file_data @@ -101,9 +103,13 @@ def test_largest_files(mocker, file_data, file_count): (TEST_FILES[2], True, False), ], ) -def test_largest_files_elements(mocker, test_file, has_format_version, has_puid): +def test_largest_files_elements( + app_instance, mocker, test_file, has_format_version, has_puid +): """Test that returned file data matches expected values. """ + app_instance.app_context().push() + mock_query = mocker.patch("AIPscan.Data.data._largest_files_query") mock_query.return_value = [test_file] From e652ff5cfa9be069a8a92e5f79ec5aa45fa5ffaa Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Mon, 2 Nov 2020 16:36:22 -0500 Subject: [PATCH 05/18] Add note to README about DEBUG mode --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 93efe474..197c84a7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Copyright Artefactual Systems Inc (2020). * Activate virtualenv: `source venv/bin/activate` * Install requirements (this includes Flask & Celery): `pip install -r requirements.txt` * Create database: `python create_aipscan_db.py` +* Enable DEBUG mode if desired for development: `export FLASK_CONFIG=dev` * In a terminal window, start the Flask server: `python run.py` ### RabbitMQ From b5cb8baf358282d8b6dd0441c01c74d446b18d01 Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Mon, 2 Nov 2020 17:04:03 -0500 Subject: [PATCH 06/18] Remove extraneous comment --- AIPscan/Aggregator/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/AIPscan/Aggregator/tasks.py b/AIPscan/Aggregator/tasks.py index 441aea1a..82b655f0 100644 --- a/AIPscan/Aggregator/tasks.py +++ b/AIPscan/Aggregator/tasks.py @@ -6,7 +6,6 @@ from celery.utils.log import get_task_logger -# from AIPscan.extensions import celery from AIPscan import db from AIPscan.extensions import celery from AIPscan.models import ( From c34329961d3a753850c87bc60e091041e366be4a Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Mon, 2 Nov 2020 17:45:22 -0500 Subject: [PATCH 07/18] Improve app_instance fixture This commit adds the application context to tests via the app_instance fixture so that it's not necessary to do so explicitly within tests and adds some post-test cleanup. --- AIPscan/Aggregator/tests/conftest.py | 9 ++++++--- AIPscan/Aggregator/tests/test_database_helpers.py | 6 ------ AIPscan/Data/tests/conftest.py | 9 ++++++--- AIPscan/Data/tests/test_largest_files.py | 4 ---- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/AIPscan/Aggregator/tests/conftest.py b/AIPscan/Aggregator/tests/conftest.py index eb30886c..2201abb0 100644 --- a/AIPscan/Aggregator/tests/conftest.py +++ b/AIPscan/Aggregator/tests/conftest.py @@ -17,6 +17,9 @@ def app_instance(): state to the test database from a fixture as needed for tests. """ app = create_app("test") - with app.app_context(): - db.create_all() - return app + context = app.app_context() + context.push() + db.create_all() + yield app + db.drop_all() + context.pop() diff --git a/AIPscan/Aggregator/tests/test_database_helpers.py b/AIPscan/Aggregator/tests/test_database_helpers.py index d7f47ce3..3c7a94c8 100644 --- a/AIPscan/Aggregator/tests/test_database_helpers.py +++ b/AIPscan/Aggregator/tests/test_database_helpers.py @@ -13,8 +13,6 @@ def test_create_aip(app_instance): """Test AIP creation.""" - app_instance.app_context().push() - PACKAGE_UUID = str(uuid.uuid4()) TRANSFER_NAME = "some name" STORAGE_SERVICE_ID = 1 @@ -50,8 +48,6 @@ def test_event_creation( an AIP and that they are potentially written to the database okay. Make sure too that the event_agent_relationship is established. """ - app_instance.app_context().push() - script_dir = os.path.dirname(os.path.realpath(__file__)) mets_file = os.path.join(script_dir, FIXTURES_DIR, fixture_path) mets = metsrw.METSDocument.fromfile(mets_file) @@ -89,8 +85,6 @@ def test_collect_agents(app_instance, fixture_path, number_of_unique_agents): then add to the database. Agents are "repeated" per PREMIS:OBJECT in METS. """ - app_instance.app_context().push() - script_dir = os.path.dirname(os.path.realpath(__file__)) mets_file = os.path.join(script_dir, FIXTURES_DIR, fixture_path) mets = metsrw.METSDocument.fromfile(mets_file) diff --git a/AIPscan/Data/tests/conftest.py b/AIPscan/Data/tests/conftest.py index eb30886c..2201abb0 100644 --- a/AIPscan/Data/tests/conftest.py +++ b/AIPscan/Data/tests/conftest.py @@ -17,6 +17,9 @@ def app_instance(): state to the test database from a fixture as needed for tests. """ app = create_app("test") - with app.app_context(): - db.create_all() - return app + context = app.app_context() + context.push() + db.create_all() + yield app + db.drop_all() + context.pop() diff --git a/AIPscan/Data/tests/test_largest_files.py b/AIPscan/Data/tests/test_largest_files.py index e8319750..418abd76 100644 --- a/AIPscan/Data/tests/test_largest_files.py +++ b/AIPscan/Data/tests/test_largest_files.py @@ -78,8 +78,6 @@ def test_largest_files(app_instance, mocker, file_data, file_count): """Test that return value conforms to expected structure. """ - app_instance.app_context().push() - mock_query = mocker.patch("AIPscan.Data.data._largest_files_query") mock_query.return_value = file_data @@ -108,8 +106,6 @@ def test_largest_files_elements( ): """Test that returned file data matches expected values. """ - app_instance.app_context().push() - mock_query = mocker.patch("AIPscan.Data.data._largest_files_query") mock_query.return_value = [test_file] From 132bcdc9c514f5c25c38adc3b54e7e7ed16c7e9b Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Thu, 5 Nov 2020 11:20:30 -0500 Subject: [PATCH 08/18] Improve use of conftest.py This commit replaces Blueprint-specific conftest modules with a single one that can be used across the project. It also makes use of the app.app_context context manager so that we do not need to push and pop the app context manually. --- AIPscan/Data/tests/conftest.py | 25 ---------------------- AIPscan/{Aggregator/tests => }/conftest.py | 12 +++++------ 2 files changed, 6 insertions(+), 31 deletions(-) delete mode 100644 AIPscan/Data/tests/conftest.py rename AIPscan/{Aggregator/tests => }/conftest.py (76%) diff --git a/AIPscan/Data/tests/conftest.py b/AIPscan/Data/tests/conftest.py deleted file mode 100644 index 2201abb0..00000000 --- a/AIPscan/Data/tests/conftest.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- - -import pytest - -from AIPscan import db -from AIPscan.application import create_app - - -@pytest.fixture -def app_instance(): - """Pytest fixture that returns an instance of our application. - - This fixture provides a Flask application context for tests using - AIPscan's test configuration. - - This pattern can be extended in additional fixtures to, e.g. load - state to the test database from a fixture as needed for tests. - """ - app = create_app("test") - context = app.app_context() - context.push() - db.create_all() - yield app - db.drop_all() - context.pop() diff --git a/AIPscan/Aggregator/tests/conftest.py b/AIPscan/conftest.py similarity index 76% rename from AIPscan/Aggregator/tests/conftest.py rename to AIPscan/conftest.py index 2201abb0..7cf0f443 100644 --- a/AIPscan/Aggregator/tests/conftest.py +++ b/AIPscan/conftest.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +"""This module defines shared AIPscan pytest fixtures.""" + import pytest from AIPscan import db @@ -17,9 +19,7 @@ def app_instance(): state to the test database from a fixture as needed for tests. """ app = create_app("test") - context = app.app_context() - context.push() - db.create_all() - yield app - db.drop_all() - context.pop() + with app.app_context(): + db.create_all() + yield app + db.drop_all() From 4344dbf408102ff01c05d9576823f70d42d5d386 Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Thu, 5 Nov 2020 11:25:02 -0500 Subject: [PATCH 09/18] Move create_app logic into app context manager --- AIPscan/application.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/AIPscan/application.py b/AIPscan/application.py index 1a10447c..5b3efa73 100644 --- a/AIPscan/application.py +++ b/AIPscan/application.py @@ -19,13 +19,15 @@ def create_app(config_name="default"): app.config.from_object(CONFIGS[config_name]) - app.register_blueprint(aggregator, url_prefix="/aggregator") - app.register_blueprint(reporter, url_prefix="/reporter") - app.register_blueprint(user, url_prefix="/user") - app.register_blueprint(api) - app.register_blueprint(home) + with app.app_context(): - db.init_app(app) - configure_celery(app) + app.register_blueprint(aggregator, url_prefix="/aggregator") + app.register_blueprint(reporter, url_prefix="/reporter") + app.register_blueprint(user, url_prefix="/user") + app.register_blueprint(api) + app.register_blueprint(home) - return app + db.init_app(app) + configure_celery(app) + + return app From d4b54fb4e0b7b0e6a32b249ce8bc17babfaf5539 Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Thu, 5 Nov 2020 11:30:49 -0500 Subject: [PATCH 10/18] Remove comment --- AIPscan/celery.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/AIPscan/celery.py b/AIPscan/celery.py index 2472c5a1..a1e28d6e 100644 --- a/AIPscan/celery.py +++ b/AIPscan/celery.py @@ -2,12 +2,6 @@ from AIPscan import extensions -# PICTURAE TODO: Create a different app configuration for celery. If -# we inspect the celery object below celery.__dict__ we can see all -# of the app consts have been consumed by the celery constructor, -# probably as a **kwarg and hasn't decided to rid itself of any values -# that are superfluous. - def configure_celery(app): """Add Flask app context to celery.Task.""" From 125f1ac8cdf66b35cc4cd65f0a3053e510ee30fe Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Thu, 5 Nov 2020 11:44:10 -0500 Subject: [PATCH 11/18] Automatically create database --- AIPscan/application.py | 2 ++ README.md | 1 - create_aipscan.db.py | 11 ----------- 3 files changed, 2 insertions(+), 12 deletions(-) delete mode 100755 create_aipscan.db.py diff --git a/AIPscan/application.py b/AIPscan/application.py index 5b3efa73..9d9e7644 100644 --- a/AIPscan/application.py +++ b/AIPscan/application.py @@ -30,4 +30,6 @@ def create_app(config_name="default"): db.init_app(app) configure_celery(app) + db.create_all() + return app diff --git a/README.md b/README.md index 197c84a7..1a927610 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ Copyright Artefactual Systems Inc (2020). * Set up virtualenv in the project root directory: `virtualenv venv` * Activate virtualenv: `source venv/bin/activate` * Install requirements (this includes Flask & Celery): `pip install -r requirements.txt` -* Create database: `python create_aipscan_db.py` * Enable DEBUG mode if desired for development: `export FLASK_CONFIG=dev` * In a terminal window, start the Flask server: `python run.py` diff --git a/create_aipscan.db.py b/create_aipscan.db.py deleted file mode 100755 index 5422b00f..00000000 --- a/create_aipscan.db.py +++ /dev/null @@ -1,11 +0,0 @@ -#!venv/bin/python - -# chmod a+x create_aipscan.db.py -# ./create_aipscan.db.py - -from AIPscan import db -from AIPscan.application import create_app - -app = create_app() -app.app_context().push() -db.create_all() From 2f69ff205ebf51ade84f537b000e8fcfa0e2b2a8 Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Thu, 5 Nov 2020 18:01:10 -0500 Subject: [PATCH 12/18] Move app factory to __init__ This commit moves the Application Factory into AIPscan's __init__ module. This seems to be common practice, reduces the number of new modules added in this work, and provides the potential down the line to call the Application Factory directly from gunicorn without the need for a wsgi.py entrypoint should we choose to do so. --- AIPscan/__init__.py | 32 ++++++++++++++++++++++++++++++++ AIPscan/application.py | 35 ----------------------------------- AIPscan/conftest.py | 3 +-- AIPscan/worker.py | 4 +++- run.py | 2 +- 5 files changed, 37 insertions(+), 39 deletions(-) delete mode 100644 AIPscan/application.py diff --git a/AIPscan/__init__.py b/AIPscan/__init__.py index 6aeb4916..d361f5d2 100644 --- a/AIPscan/__init__.py +++ b/AIPscan/__init__.py @@ -1,5 +1,37 @@ # -*- coding: utf-8 -*- +from flask import Flask from flask_sqlalchemy import SQLAlchemy +from AIPscan.celery import configure_celery +from config import CONFIGS + db = SQLAlchemy() + + +def create_app(config_name="default"): + """Flask app factory, returns app instance.""" + app = Flask(__name__) + + app.config.from_object(CONFIGS[config_name]) + + with app.app_context(): + + from AIPscan.Aggregator.views import aggregator + from AIPscan.Reporter.views import reporter + from AIPscan.User.views import user + from AIPscan.API.views import api + from AIPscan.Home.views import home + + app.register_blueprint(aggregator, url_prefix="/aggregator") + app.register_blueprint(reporter, url_prefix="/reporter") + app.register_blueprint(user, url_prefix="/user") + app.register_blueprint(api) + app.register_blueprint(home) + + db.init_app(app) + configure_celery(app) + + db.create_all() + + return app diff --git a/AIPscan/application.py b/AIPscan/application.py deleted file mode 100644 index 9d9e7644..00000000 --- a/AIPscan/application.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- - -from flask import Flask - -from AIPscan.Aggregator.views import aggregator -from AIPscan.Reporter.views import reporter -from AIPscan.User.views import user -from AIPscan.API.views import api -from AIPscan.Home.views import home - -from AIPscan import db -from AIPscan.celery import configure_celery -from config import CONFIGS - - -def create_app(config_name="default"): - """Flask app factory, returns app instance.""" - app = Flask(__name__) - - app.config.from_object(CONFIGS[config_name]) - - with app.app_context(): - - app.register_blueprint(aggregator, url_prefix="/aggregator") - app.register_blueprint(reporter, url_prefix="/reporter") - app.register_blueprint(user, url_prefix="/user") - app.register_blueprint(api) - app.register_blueprint(home) - - db.init_app(app) - configure_celery(app) - - db.create_all() - - return app diff --git a/AIPscan/conftest.py b/AIPscan/conftest.py index 7cf0f443..406b42d8 100644 --- a/AIPscan/conftest.py +++ b/AIPscan/conftest.py @@ -4,8 +4,7 @@ import pytest -from AIPscan import db -from AIPscan.application import create_app +from AIPscan import db, create_app @pytest.fixture diff --git a/AIPscan/worker.py b/AIPscan/worker.py index 5db56712..41f339f4 100644 --- a/AIPscan/worker.py +++ b/AIPscan/worker.py @@ -1,4 +1,6 @@ -from AIPscan.application import create_app +# -*- coding: utf-8 -*- + +from AIPscan import create_app from AIPscan.celery import configure_celery app = create_app() diff --git a/run.py b/run.py index 5fc20f93..09602b74 100644 --- a/run.py +++ b/run.py @@ -1,6 +1,6 @@ import os -from AIPscan.application import create_app +from AIPscan import create_app if __name__ == "__main__": From 54a6cd641e092bb9eb9ca7f7d78ab886b330e5b2 Mon Sep 17 00:00:00 2001 From: Tessa Walsh Date: Thu, 5 Nov 2020 18:16:19 -0500 Subject: [PATCH 13/18] Add module docstrings --- AIPscan/celery.py | 2 ++ AIPscan/extensions.py | 10 ++++++++-- AIPscan/worker.py | 7 +++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/AIPscan/celery.py b/AIPscan/celery.py index a1e28d6e..806eaeb8 100644 --- a/AIPscan/celery.py +++ b/AIPscan/celery.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +"""This module contains code related to Celery configuration.""" + from AIPscan import extensions diff --git a/AIPscan/extensions.py b/AIPscan/extensions.py index 36de9b0d..5916ecc0 100644 --- a/AIPscan/extensions.py +++ b/AIPscan/extensions.py @@ -1,11 +1,17 @@ # -*- coding: utf-8 -*- +"""This module contains code related to Flask extensions. + +The Celery instance that is initialized here is lacking application +context, which will be provided via AIPscan.celery's configure_celery +function. +""" + from celery import Celery from celery_config import CELERY_RESULT_BACKEND, CELERY_BROKER_URL -# Celery instance that will be initialized at import time and then -# further configured via AIPscan.celery's configure_celery method. + celery = Celery( "tasks", backend=CELERY_RESULT_BACKEND, diff --git a/AIPscan/worker.py b/AIPscan/worker.py index 41f339f4..99a99cf2 100644 --- a/AIPscan/worker.py +++ b/AIPscan/worker.py @@ -1,5 +1,12 @@ # -*- coding: utf-8 -*- +"""This module defines and initalizes a Celery worker. + +Since Celery workers are run separately from the Flask application (for +example via a systemd service), we use our Application Factory function +to provide application context. +""" + from AIPscan import create_app from AIPscan.celery import configure_celery From ac573bce0e356060bda1b5ae5be9f7d047fde62f Mon Sep 17 00:00:00 2001 From: Ross Spencer Date: Fri, 13 Nov 2020 17:34:08 -0500 Subject: [PATCH 14/18] Update data module to satisfy Sphinx --- AIPscan/Data/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIPscan/Data/data.py b/AIPscan/Data/data.py index 43b0d7bf..9672d320 100644 --- a/AIPscan/Data/data.py +++ b/AIPscan/Data/data.py @@ -247,7 +247,7 @@ def largest_files(storage_service_id, file_type=None, limit=20): :param storage_service_id: Storage Service ID. :param file_type: Optional filter for type of file to return - (acceptable values are "original" or "preservation"). + (acceptable values are "original" or "preservation"). :param limit: Upper limit of number of results to return. :returns: "report" dict containing following fields: From 7ed5fd07f46a99408988685a3f0b9592e95e1472 Mon Sep 17 00:00:00 2001 From: Ross Spencer Date: Fri, 13 Nov 2020 17:38:48 -0500 Subject: [PATCH 15/18] Remove an unneeded comment to satisfy Sphinx --- AIPscan/API/namespace_data.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/AIPscan/API/namespace_data.py b/AIPscan/API/namespace_data.py index a45dcc41..85b47aeb 100644 --- a/AIPscan/API/namespace_data.py +++ b/AIPscan/API/namespace_data.py @@ -14,13 +14,6 @@ api = Namespace("data", description="Retrieve data from AIPscan to shape as you desire") -""" -data = api.model('Data', { - 'id': fields.String(required=True, description='Do we need this? An identifier for the data...'), - 'name': fields.String(required=True, description='Do we need this? A name for the datas...'), -}) -""" - def parse_bool(val, default=True): try: From 087ef340b49907cd7d15d2ccae4c26f5451b416f Mon Sep 17 00:00:00 2001 From: Ross Spencer Date: Fri, 13 Nov 2020 17:43:00 -0500 Subject: [PATCH 16/18] Create a basic conf.py for Sphinx --- docs/conf.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/conf.py diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..afe5a8d1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,68 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'AIPscan' +copyright = '2020, Artefactual Systems Inc.' +author = 'Artefactual Systems Inc.' + +# The full version, including alpha/beta/rc tags (TODO: When the project has a +# version module, we will import that above, and then use its value below. +release = '0.x' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinxcontrib.mermaid", + "releases", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" +html_theme_options = { + "description": "Reporting for Archivematica", + "fixed_sidebar": True, + "github_user": "artefactual-labs", + "github_repo": "AIPscan", + "github_banner": True, + "github_button": False, +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] From 2acc1e6b621e2f6221fa5a662ebb4f42cb7d633c Mon Sep 17 00:00:00 2001 From: Ross Spencer Date: Fri, 13 Nov 2020 17:43:23 -0500 Subject: [PATCH 17/18] Create initial Sphinx documentation of AIPscan --- docs/AIPscan.API.rst | 37 + docs/AIPscan.Aggregator.rst | 85 + docs/AIPscan.Aggregator.tests.rst | 45 + docs/AIPscan.Data.rst | 29 + docs/AIPscan.Data.tests.rst | 21 + docs/AIPscan.Home.rst | 21 + docs/AIPscan.Reporter.rst | 61 + docs/AIPscan.User.rst | 29 + docs/AIPscan.rst | 74 + docs/Makefile | 20 + docs/_build/doctrees/AIPscan.API.doctree | Bin 0 -> 40936 bytes .../doctrees/AIPscan.Aggregator.doctree | Bin 0 -> 106832 bytes .../doctrees/AIPscan.Aggregator.tests.doctree | Bin 0 -> 42235 bytes docs/_build/doctrees/AIPscan.Data.doctree | Bin 0 -> 17155 bytes .../doctrees/AIPscan.Data.tests.doctree | Bin 0 -> 10189 bytes docs/_build/doctrees/AIPscan.Home.doctree | Bin 0 -> 5763 bytes docs/_build/doctrees/AIPscan.Reporter.doctree | Bin 0 -> 33247 bytes docs/_build/doctrees/AIPscan.User.doctree | Bin 0 -> 12291 bytes docs/_build/doctrees/AIPscan.doctree | Bin 0 -> 102804 bytes docs/_build/doctrees/environment.pickle | Bin 0 -> 374725 bytes docs/_build/doctrees/index.doctree | Bin 0 -> 5937 bytes docs/_build/doctrees/modules.doctree | Bin 0 -> 2691 bytes docs/_build/doctrees/overview.doctree | Bin 0 -> 2460 bytes docs/_build/html/.buildinfo | 4 + docs/_build/html/AIPscan.API.html | 267 + docs/_build/html/AIPscan.Aggregator.html | 511 + .../_build/html/AIPscan.Aggregator.tests.html | 265 + docs/_build/html/AIPscan.Data.html | 207 + docs/_build/html/AIPscan.Data.tests.html | 168 + docs/_build/html/AIPscan.Home.html | 154 + docs/_build/html/AIPscan.Reporter.html | 258 + docs/_build/html/AIPscan.User.html | 175 + docs/_build/html/AIPscan.html | 692 + docs/_build/html/_modules/AIPscan.html | 147 + .../_modules/AIPscan/API/namespace_data.html | 221 + .../_modules/AIPscan/API/namespace_infos.html | 147 + .../AIPscan/Aggregator/celery_helpers.html | 126 + .../AIPscan/Aggregator/database_helpers.html | 403 + .../_modules/AIPscan/Aggregator/forms.html | 127 + .../Aggregator/mets_parse_helpers.html | 216 + .../AIPscan/Aggregator/task_helpers.html | 249 + .../_modules/AIPscan/Aggregator/tasks.html | 419 + .../tests/test_database_helpers.html | 204 + .../AIPscan/Aggregator/tests/test_mets.html | 168 + .../Aggregator/tests/test_task_helpers.html | 331 + .../AIPscan/Aggregator/tests/test_types.html | 268 + .../_modules/AIPscan/Aggregator/types.html | 258 + .../_modules/AIPscan/Aggregator/views.html | 408 + .../html/_modules/AIPscan/Data/data.html | 406 + .../Data/tests/test_largest_files.html | 250 + .../html/_modules/AIPscan/Home/views.html | 123 + .../_modules/AIPscan/Reporter/helpers.html | 151 + .../AIPscan/Reporter/report_aip_contents.html | 166 + .../Reporter/report_formats_count.html | 343 + .../Reporter/report_largest_files.html | 152 + .../report_originals_with_derivatives.html | 162 + .../html/_modules/AIPscan/Reporter/views.html | 260 + .../html/_modules/AIPscan/User/forms.html | 124 + docs/_build/html/_modules/AIPscan/celery.html | 132 + .../html/_modules/AIPscan/conftest.html | 136 + .../_build/html/_modules/AIPscan/helpers.html | 122 + docs/_build/html/_modules/AIPscan/models.html | 350 + .../_build/html/_modules/flask_restx/api.html | 1037 ++ .../html/_modules/flask_restx/namespace.html | 486 + docs/_build/html/_modules/index.html | 141 + .../_modules/sqlalchemy/orm/attributes.html | 2159 +++ .../html/_modules/wtforms/fields/core.html | 1117 ++ docs/_build/html/_sources/AIPscan.API.rst.txt | 37 + .../html/_sources/AIPscan.Aggregator.rst.txt | 85 + .../_sources/AIPscan.Aggregator.tests.rst.txt | 45 + .../_build/html/_sources/AIPscan.Data.rst.txt | 29 + .../html/_sources/AIPscan.Data.tests.rst.txt | 21 + .../_build/html/_sources/AIPscan.Home.rst.txt | 21 + .../html/_sources/AIPscan.Reporter.rst.txt | 61 + .../_build/html/_sources/AIPscan.User.rst.txt | 29 + docs/_build/html/_sources/AIPscan.rst.txt | 74 + docs/_build/html/_sources/index.rst.txt | 30 + docs/_build/html/_sources/modules.rst.txt | 11 + docs/_build/html/_sources/overview.rst.txt | 4 + docs/_build/html/_static/alabaster.css | 707 + docs/_build/html/_static/basic.css | 856 ++ docs/_build/html/_static/custom.css | 1 + docs/_build/html/_static/doctools.js | 316 + .../html/_static/documentation_options.js | 12 + docs/_build/html/_static/file.png | Bin 0 -> 286 bytes docs/_build/html/_static/jquery-3.5.1.js | 10872 ++++++++++++++++ docs/_build/html/_static/jquery.js | 2 + docs/_build/html/_static/language_data.js | 297 + docs/_build/html/_static/minus.png | Bin 0 -> 90 bytes docs/_build/html/_static/plus.png | Bin 0 -> 90 bytes docs/_build/html/_static/pygments.css | 82 + docs/_build/html/_static/searchtools.js | 514 + docs/_build/html/_static/underscore-1.3.1.js | 999 ++ docs/_build/html/_static/underscore.js | 31 + docs/_build/html/genindex.html | 1047 ++ docs/_build/html/index.html | 191 + docs/_build/html/modules.html | 254 + docs/_build/html/objects.inv | Bin 0 -> 2129 bytes docs/_build/html/overview.html | 118 + docs/_build/html/py-modindex.html | 328 + docs/_build/html/search.html | 122 + docs/_build/html/searchindex.js | 1 + docs/index.rst | 30 + docs/make.bat | 35 + docs/modules.rst | 11 + docs/overview.rst | 4 + 106 files changed, 31909 insertions(+) create mode 100644 docs/AIPscan.API.rst create mode 100644 docs/AIPscan.Aggregator.rst create mode 100644 docs/AIPscan.Aggregator.tests.rst create mode 100644 docs/AIPscan.Data.rst create mode 100644 docs/AIPscan.Data.tests.rst create mode 100644 docs/AIPscan.Home.rst create mode 100644 docs/AIPscan.Reporter.rst create mode 100644 docs/AIPscan.User.rst create mode 100644 docs/AIPscan.rst create mode 100644 docs/Makefile create mode 100644 docs/_build/doctrees/AIPscan.API.doctree create mode 100644 docs/_build/doctrees/AIPscan.Aggregator.doctree create mode 100644 docs/_build/doctrees/AIPscan.Aggregator.tests.doctree create mode 100644 docs/_build/doctrees/AIPscan.Data.doctree create mode 100644 docs/_build/doctrees/AIPscan.Data.tests.doctree create mode 100644 docs/_build/doctrees/AIPscan.Home.doctree create mode 100644 docs/_build/doctrees/AIPscan.Reporter.doctree create mode 100644 docs/_build/doctrees/AIPscan.User.doctree create mode 100644 docs/_build/doctrees/AIPscan.doctree create mode 100644 docs/_build/doctrees/environment.pickle create mode 100644 docs/_build/doctrees/index.doctree create mode 100644 docs/_build/doctrees/modules.doctree create mode 100644 docs/_build/doctrees/overview.doctree create mode 100644 docs/_build/html/.buildinfo create mode 100644 docs/_build/html/AIPscan.API.html create mode 100644 docs/_build/html/AIPscan.Aggregator.html create mode 100644 docs/_build/html/AIPscan.Aggregator.tests.html create mode 100644 docs/_build/html/AIPscan.Data.html create mode 100644 docs/_build/html/AIPscan.Data.tests.html create mode 100644 docs/_build/html/AIPscan.Home.html create mode 100644 docs/_build/html/AIPscan.Reporter.html create mode 100644 docs/_build/html/AIPscan.User.html create mode 100644 docs/_build/html/AIPscan.html create mode 100644 docs/_build/html/_modules/AIPscan.html create mode 100644 docs/_build/html/_modules/AIPscan/API/namespace_data.html create mode 100644 docs/_build/html/_modules/AIPscan/API/namespace_infos.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/celery_helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/database_helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/forms.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/mets_parse_helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/task_helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/tasks.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/tests/test_database_helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/tests/test_mets.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/tests/test_task_helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/tests/test_types.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/types.html create mode 100644 docs/_build/html/_modules/AIPscan/Aggregator/views.html create mode 100644 docs/_build/html/_modules/AIPscan/Data/data.html create mode 100644 docs/_build/html/_modules/AIPscan/Data/tests/test_largest_files.html create mode 100644 docs/_build/html/_modules/AIPscan/Home/views.html create mode 100644 docs/_build/html/_modules/AIPscan/Reporter/helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/Reporter/report_aip_contents.html create mode 100644 docs/_build/html/_modules/AIPscan/Reporter/report_formats_count.html create mode 100644 docs/_build/html/_modules/AIPscan/Reporter/report_largest_files.html create mode 100644 docs/_build/html/_modules/AIPscan/Reporter/report_originals_with_derivatives.html create mode 100644 docs/_build/html/_modules/AIPscan/Reporter/views.html create mode 100644 docs/_build/html/_modules/AIPscan/User/forms.html create mode 100644 docs/_build/html/_modules/AIPscan/celery.html create mode 100644 docs/_build/html/_modules/AIPscan/conftest.html create mode 100644 docs/_build/html/_modules/AIPscan/helpers.html create mode 100644 docs/_build/html/_modules/AIPscan/models.html create mode 100644 docs/_build/html/_modules/flask_restx/api.html create mode 100644 docs/_build/html/_modules/flask_restx/namespace.html create mode 100644 docs/_build/html/_modules/index.html create mode 100644 docs/_build/html/_modules/sqlalchemy/orm/attributes.html create mode 100644 docs/_build/html/_modules/wtforms/fields/core.html create mode 100644 docs/_build/html/_sources/AIPscan.API.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.Aggregator.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.Aggregator.tests.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.Data.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.Data.tests.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.Home.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.Reporter.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.User.rst.txt create mode 100644 docs/_build/html/_sources/AIPscan.rst.txt create mode 100644 docs/_build/html/_sources/index.rst.txt create mode 100644 docs/_build/html/_sources/modules.rst.txt create mode 100644 docs/_build/html/_sources/overview.rst.txt create mode 100644 docs/_build/html/_static/alabaster.css create mode 100644 docs/_build/html/_static/basic.css create mode 100644 docs/_build/html/_static/custom.css create mode 100644 docs/_build/html/_static/doctools.js create mode 100644 docs/_build/html/_static/documentation_options.js create mode 100644 docs/_build/html/_static/file.png create mode 100644 docs/_build/html/_static/jquery-3.5.1.js create mode 100644 docs/_build/html/_static/jquery.js create mode 100644 docs/_build/html/_static/language_data.js create mode 100644 docs/_build/html/_static/minus.png create mode 100644 docs/_build/html/_static/plus.png create mode 100644 docs/_build/html/_static/pygments.css create mode 100644 docs/_build/html/_static/searchtools.js create mode 100644 docs/_build/html/_static/underscore-1.3.1.js create mode 100644 docs/_build/html/_static/underscore.js create mode 100644 docs/_build/html/genindex.html create mode 100644 docs/_build/html/index.html create mode 100644 docs/_build/html/modules.html create mode 100644 docs/_build/html/objects.inv create mode 100644 docs/_build/html/overview.html create mode 100644 docs/_build/html/py-modindex.html create mode 100644 docs/_build/html/search.html create mode 100644 docs/_build/html/searchindex.js create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/modules.rst create mode 100644 docs/overview.rst diff --git a/docs/AIPscan.API.rst b/docs/AIPscan.API.rst new file mode 100644 index 00000000..7f30ec71 --- /dev/null +++ b/docs/AIPscan.API.rst @@ -0,0 +1,37 @@ +AIPscan.API package +=================== + +Submodules +---------- + +AIPscan.API.namespace\_data module +---------------------------------- + +.. automodule:: AIPscan.API.namespace_data + :members: + :undoc-members: + :show-inheritance: + +AIPscan.API.namespace\_infos module +----------------------------------- + +.. automodule:: AIPscan.API.namespace_infos + :members: + :undoc-members: + :show-inheritance: + +AIPscan.API.views module +------------------------ + +.. automodule:: AIPscan.API.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.API + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.Aggregator.rst b/docs/AIPscan.Aggregator.rst new file mode 100644 index 00000000..9acf8653 --- /dev/null +++ b/docs/AIPscan.Aggregator.rst @@ -0,0 +1,85 @@ +AIPscan.Aggregator package +========================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + AIPscan.Aggregator.tests + +Submodules +---------- + +AIPscan.Aggregator.celery\_helpers module +----------------------------------------- + +.. automodule:: AIPscan.Aggregator.celery_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.database\_helpers module +------------------------------------------- + +.. automodule:: AIPscan.Aggregator.database_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.forms module +------------------------------- + +.. automodule:: AIPscan.Aggregator.forms + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.mets\_parse\_helpers module +---------------------------------------------- + +.. automodule:: AIPscan.Aggregator.mets_parse_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.task\_helpers module +--------------------------------------- + +.. automodule:: AIPscan.Aggregator.task_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tasks module +------------------------------- + +.. automodule:: AIPscan.Aggregator.tasks + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.types module +------------------------------- + +.. automodule:: AIPscan.Aggregator.types + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.views module +------------------------------- + +.. automodule:: AIPscan.Aggregator.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Aggregator + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.Aggregator.tests.rst b/docs/AIPscan.Aggregator.tests.rst new file mode 100644 index 00000000..282a1a1b --- /dev/null +++ b/docs/AIPscan.Aggregator.tests.rst @@ -0,0 +1,45 @@ +AIPscan.Aggregator.tests package +================================ + +Submodules +---------- + +AIPscan.Aggregator.tests.test\_database\_helpers module +------------------------------------------------------- + +.. automodule:: AIPscan.Aggregator.tests.test_database_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tests.test\_mets module +------------------------------------------ + +.. automodule:: AIPscan.Aggregator.tests.test_mets + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tests.test\_task\_helpers module +--------------------------------------------------- + +.. automodule:: AIPscan.Aggregator.tests.test_task_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tests.test\_types module +------------------------------------------- + +.. automodule:: AIPscan.Aggregator.tests.test_types + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Aggregator.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.Data.rst b/docs/AIPscan.Data.rst new file mode 100644 index 00000000..af61dfd2 --- /dev/null +++ b/docs/AIPscan.Data.rst @@ -0,0 +1,29 @@ +AIPscan.Data package +==================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + AIPscan.Data.tests + +Submodules +---------- + +AIPscan.Data.data module +------------------------ + +.. automodule:: AIPscan.Data.data + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Data + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.Data.tests.rst b/docs/AIPscan.Data.tests.rst new file mode 100644 index 00000000..248701db --- /dev/null +++ b/docs/AIPscan.Data.tests.rst @@ -0,0 +1,21 @@ +AIPscan.Data.tests package +========================== + +Submodules +---------- + +AIPscan.Data.tests.test\_largest\_files module +---------------------------------------------- + +.. automodule:: AIPscan.Data.tests.test_largest_files + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Data.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.Home.rst b/docs/AIPscan.Home.rst new file mode 100644 index 00000000..605a4308 --- /dev/null +++ b/docs/AIPscan.Home.rst @@ -0,0 +1,21 @@ +AIPscan.Home package +==================== + +Submodules +---------- + +AIPscan.Home.views module +------------------------- + +.. automodule:: AIPscan.Home.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Home + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.Reporter.rst b/docs/AIPscan.Reporter.rst new file mode 100644 index 00000000..cd0d31f6 --- /dev/null +++ b/docs/AIPscan.Reporter.rst @@ -0,0 +1,61 @@ +AIPscan.Reporter package +======================== + +Submodules +---------- + +AIPscan.Reporter.helpers module +------------------------------- + +.. automodule:: AIPscan.Reporter.helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_aip\_contents module +--------------------------------------------- + +.. automodule:: AIPscan.Reporter.report_aip_contents + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_formats\_count module +---------------------------------------------- + +.. automodule:: AIPscan.Reporter.report_formats_count + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_largest\_files module +---------------------------------------------- + +.. automodule:: AIPscan.Reporter.report_largest_files + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_originals\_with\_derivatives module +------------------------------------------------------------ + +.. automodule:: AIPscan.Reporter.report_originals_with_derivatives + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.views module +----------------------------- + +.. automodule:: AIPscan.Reporter.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Reporter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.User.rst b/docs/AIPscan.User.rst new file mode 100644 index 00000000..71478bb4 --- /dev/null +++ b/docs/AIPscan.User.rst @@ -0,0 +1,29 @@ +AIPscan.User package +==================== + +Submodules +---------- + +AIPscan.User.forms module +------------------------- + +.. automodule:: AIPscan.User.forms + :members: + :undoc-members: + :show-inheritance: + +AIPscan.User.views module +------------------------- + +.. automodule:: AIPscan.User.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.User + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/AIPscan.rst b/docs/AIPscan.rst new file mode 100644 index 00000000..5ea3b0db --- /dev/null +++ b/docs/AIPscan.rst @@ -0,0 +1,74 @@ +AIPscan package +=============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + AIPscan.API + AIPscan.Aggregator + AIPscan.Data + AIPscan.Home + AIPscan.Reporter + AIPscan.User + +Submodules +---------- + +AIPscan.celery module +--------------------- + +.. automodule:: AIPscan.celery + :members: + :undoc-members: + :show-inheritance: + +AIPscan.conftest module +----------------------- + +.. automodule:: AIPscan.conftest + :members: + :undoc-members: + :show-inheritance: + +AIPscan.extensions module +------------------------- + +.. automodule:: AIPscan.extensions + :members: + :undoc-members: + :show-inheritance: + +AIPscan.helpers module +---------------------- + +.. automodule:: AIPscan.helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.models module +--------------------- + +.. automodule:: AIPscan.models + :members: + :undoc-members: + :show-inheritance: + +AIPscan.worker module +--------------------- + +.. automodule:: AIPscan.worker + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_build/doctrees/AIPscan.API.doctree b/docs/_build/doctrees/AIPscan.API.doctree new file mode 100644 index 0000000000000000000000000000000000000000..aa82adb87de142cc987448a29909f1d46e8e091c GIT binary patch literal 40936 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 F{{_%}%<%vK literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/AIPscan.Aggregator.doctree b/docs/_build/doctrees/AIPscan.Aggregator.doctree new file mode 100644 index 0000000000000000000000000000000000000000..646728a8c1844143988a7fd87907172b5873f432 GIT binary patch literal 106832 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 bU_w!$!e^=EJt~=-*YO5H#t2;rwVVGxVJuw3 literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/AIPscan.Data.doctree b/docs/_build/doctrees/AIPscan.Data.doctree new file mode 100644 index 0000000000000000000000000000000000000000..fd8bdec40659f3306b4a940cdd9df0ef2c64da2d GIT binary patch literal 17155 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 literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/AIPscan.Home.doctree b/docs/_build/doctrees/AIPscan.Home.doctree new file mode 100644 index 0000000000000000000000000000000000000000..7fbeb42cb737d5edf10937dfc740d96cf156be7d GIT binary patch literal 5763 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 literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/AIPscan.doctree b/docs/_build/doctrees/AIPscan.doctree new file mode 100644 index 0000000000000000000000000000000000000000..5856fd98fad6168bcb4cf7b6f9342cb03aaba252 GIT binary patch literal 102804 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 AnE(I) literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle new file mode 100644 index 0000000000000000000000000000000000000000..f1b6c15e8ea204c452adcc6c506191736c100451 GIT binary patch literal 374725 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 z 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 z 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 S+ZnSR57{$6vO-g literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/modules.doctree b/docs/_build/doctrees/modules.doctree new file mode 100644 index 0000000000000000000000000000000000000000..d5a0e81137ddd9d2777b3f6a98fb4b59b91a282f GIT binary patch literal 2691 zcmZ8jTW{Mo6i$=)8r!*a-Aj|97`7o>7kdNtFzjUux?v~+1Vu6IE#PR8Hi=N8MpCi! zP+)s#(E@xc_kZ_Ml4Uyv1dAUJ&-FWp`lt2ZvcFUQ{GrW-%$8%ySf&}b!FQQXij-$A zd<e}xsnelL4 zOsVfv=Zu^d4$(yK_aw8?(@vtOwE*$n%4k}>by<O#?}oz#I%kRUrrH7gXiV`Jg`+0||_ zMXcRKr@1aNwvmu3S7Ha&b>a-)Ojf=38KM%;kgd=0e1YdHJm+|x`c7>J(IQPomNA}R zuao&MT>FFCv+?Dtmmva!74R!{v_wu;$mM))_?$XzO8jVrzvj6{z!BwsAhSdjjL#%O zMC#7P&*D+hUPi^rkk0I=f7Ro zIO+kFc%#C1sLV^2(pAzop*sIjOCMVHDyEd)pwQc%Y?^KOE1t)*kNhYpOyoCdcs*Dl z6~BGga_3|=N24A$oi{T&a~w4C?LVn1P=i(LioSLioS3(%_9*iGX%PC zvFz8&!YHhU$QX(J5O-m&71Mnm{K-sfmubg0Hm@G}VQhQUi!5i_qyfmtf`B;z$Ky1y zQ2De%xZ)*37Sai_>PlFw57cc6UYe3+MDaVW!qu+t3#!~|+49GHDUmBjU18T~i}7hO zhq=*$nhd#ugyA#=(sPC;JO&ls`;pA@!Vz?^W{?WLSGd{5j~7aKDF4{-L>orXeIN(S#Bp%#+lkI*h!N?KjN}-1av4P1k7(gESwntTf>A?O zq&aC=hpd)>1mR3ZRaAIbB1mm!W9`>aZh65pS*DRsM|_!U%gN@+k|K#Cchbr@VxWsK zY3WqOKx!{Lt8Ng_{Q$vO@SoO0WLS%l)#&k~oaY2FVQgSDM_&dJnNpaJ#P-jGbGiL# zGFdDZ5 z%8b(*m!srvR+eYm5%ft*c5jZ`MW^xTAX}buB#M2w#4j+Qkw?z(T;+0;qV=T+~6LH z7r#H*LaRF-Ftp8@MLV(|p9jD@5v<y_MDS(8-p5j78((9Dedp zSG|KP70sUD`y<}t{vgpxA+d-7)4ubf#y=wXx8XGyk54c35I&nM=1gU<>G>`Apn=Ph Q^;sN4b;ri5v6znk2Wa3b*8l(j literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/overview.doctree b/docs/_build/doctrees/overview.doctree new file mode 100644 index 0000000000000000000000000000000000000000..a1cfc35abe31f41ea931d16bcce1abb7d9f2755d GIT binary patch literal 2460 zcma)8&1)M+6i;mH%a&z3u5*c9MB9|KsihoxC_MyBPsN~?5_${6?CwZ2cy?wxGi%9* zKzj%zFqe|)|J&c}YS)sXP@zRTZ{GL&z4!EI=byg^kE)M9vIQ4vHKAFibjED({Yn)y8` z6brL5p?;q_XGBsu#1sAB6`75`_EJf$1rhHZMf3WtpOwY^A))DwVe`mS`kteUcop~& zf5wN=B0u+6Q?7G1HQL%sTQHR}GhGNbrN*&2OR?0-4l- z%=kr27yleN;$tNL3p`)p`5Mm)#LvIw-|=()65j4L@s|l}1$@78F3?7Ji?*R?9}0GB zyOlzO|KFC|vhtVFiN}7FmL`^+wB;T)l)CJ`Z<%vKEr8&Y7Ls$Lm18OkzWY0sCFQA5@u>)m)oAfHJZ`XF*b($2PN2`K+ce$tnt23dczUR#;pMtkFC3L&FMf z9Fgpn$#A{n_c@jAvExtJN+4H`y3#gG9pA}VQZ8U_w4_EMSKu&==AaCmp(%?&g%5rt zR8cwtVCxKA!4FC|zx?6S3cR0hUlL}FHiT1^NoJ7e=xXYMKhmY!*3zl81b^}1=79>n z@S|Gky#tNt`S!&FNq-yzu*eceek1}4{!mi2DA6xScZTy0rj|c7EY&6>8AJDGFe8AB zneV1r%@HHgAu>{63W!zUT|c6w)8wAycLWf1n3Z`!TGSz{6+r-SMo}3R9#s^iR@qql zds1OpnQ5}hBc6`gs?e5^P00!(sUx?-iW;9sBomelaHIIXcY1atGs40pMNiGiaD@Siyz zuKWH1Xv#_3SgrfSVhpnI(Lb%c5y~jxwP5086VqqU(Ox zI>LKsbBu2CM~U9GkUXu<^^r)O(sc$eQjTGrqP}%cp<;3P5J=yY1)Pbyuv&2IUjGP? z=v1gVO5HU7lQ<4+nQZeNvpiuL`tc67IAadl&QhuA4PiMIl3+g@R_(42ewV4+xafSp z9s}q*ph&1p0N0KpxUC=s}E#oo41Eeehv8Kd!YWQk5Hz_qAoJYa( zM9a4N73*gYh%NVEOXHxiS_wVb(vPudT%5ya4|T0?9{GJVd#cOW%E2F`T1q4qF<{#F uep=H%A^7*96f}86Bcu(HrV$37WtqhS0&re;uu + + + + + + AIPscan.API package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +

+ + +
+
+ + +
+ +
+

AIPscan.API package

+
+

Submodules

+
+
+

AIPscan.API.namespace_data module

+

Data namespace

+

Return ‘unkempt’ data from AIPscan so that it can be filtered, cut, and +remixed as the caller desires. Data is ‘unkempt’ not raw. No data is +raw. No data is without bias.

+
+
+class AIPscan.API.namespace_data.AIPList(api=None, *args, **kwargs)[source]
+

Bases: flask_restx.resource.Resource

+
+
+get(storage_service_id)[source]
+

AIP overview two

+
+ +
+
+methods = {'GET'}
+
+ +
+ +
+
+class AIPscan.API.namespace_data.DerivativeList(api=None, *args, **kwargs)[source]
+

Bases: flask_restx.resource.Resource

+
+
+get(storage_service_id)[source]
+

AIP overview two

+
+ +
+
+methods = {'GET'}
+
+ +
+ +
+
+class AIPscan.API.namespace_data.FMTList(api=None, *args, **kwargs)[source]
+

Bases: flask_restx.resource.Resource

+
+
+get(storage_service_id)[source]
+

AIP overview One

+
+ +
+
+methods = {'GET'}
+
+ +
+ +
+
+class AIPscan.API.namespace_data.LargestFileList(api=None, *args, **kwargs)[source]
+

Bases: flask_restx.resource.Resource

+
+
+get(storage_service_id, file_type=None, limit=20)[source]
+

Largest files

+
+ +
+
+methods = {'GET'}
+
+ +
+ +
+
+AIPscan.API.namespace_data.parse_bool(val, default=True)[source]
+
+ +
+
+

AIPscan.API.namespace_infos module

+

Infos namespace

+

At the moment, this is just a demonstration endpoint to show how +a namespace can be created and used. We only use it to return version +from the application itself.

+
+
+class AIPscan.API.namespace_infos.Version(api=None, *args, **kwargs)[source]
+

Bases: flask_restx.resource.Resource

+
+
+get()[source]
+

Get the application version

+
+ +
+
+methods = {'GET'}
+
+ +
+ +
+
+

AIPscan.API.views module

+

AIPscan API entry point

+

Provides metadata to the swagger endpoint as well as importing the +different namespace modules required to provide all the functionality +we’d like from AIPscan.

+

HOWTO extend: Proposal is to evolve the API. So avoid versioning, and +instead, add fields to endpoints. Add endpoints. And when absolutely +necessary, following good communication with stakeholders, sunset, and +deprecate endpoints.

+
+
+

Module contents

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.Aggregator.html b/docs/_build/html/AIPscan.Aggregator.html new file mode 100644 index 00000000..eae167a5 --- /dev/null +++ b/docs/_build/html/AIPscan.Aggregator.html @@ -0,0 +1,511 @@ + + + + + + + + AIPscan.Aggregator package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan.Aggregator package

+ +
+

Submodules

+
+
+

AIPscan.Aggregator.celery_helpers module

+
+
+AIPscan.Aggregator.celery_helpers.write_celery_update(package_lists_task, workflow_coordinator)[source]
+
+ +
+
+

AIPscan.Aggregator.database_helpers module

+

Functions to help us tease apart a METS file and write to the +database.

+
+
+AIPscan.Aggregator.database_helpers.collect_mets_agents(mets)[source]
+

Collect all of the unique agents in the METS file to write to the +database.

+
+ +
+
+AIPscan.Aggregator.database_helpers.create_agent_objects(unique_agents)[source]
+

Add our agents to the database. The list is already the +equivalent of a set by the time it reaches here and so we don’t +need to perform any de-duplication.

+
+ +
+
+AIPscan.Aggregator.database_helpers.create_aip_object(package_uuid, transfer_name, create_date, storage_service_id, fetch_job_id)[source]
+

Create an AIP object and save it to the database.

+
+ +
+
+AIPscan.Aggregator.database_helpers.create_event_objects(fs_entry, file_id)[source]
+

Add information about PREMIS Events associated with file to database

+
+
Parameters
+
    +
  • fs_entry – mets-reader-writer FSEntry object

  • +
  • file_id – File ID

  • +
+
+
+
+ +
+
+AIPscan.Aggregator.database_helpers.process_aip_data(aip, mets)[source]
+

Populate database with information needed for reporting from METS file

+
+
Parameters
+
    +
  • aip – AIP object

  • +
  • mets – mets-reader-writer METSDocument object

  • +
+
+
+
+ +
+
+

AIPscan.Aggregator.forms module

+
+
+class AIPscan.Aggregator.forms.StorageServiceForm(*args, **kwargs)[source]
+

Bases: flask_wtf.form.FlaskForm

+
+
+api_key = <UnboundField(StringField, ('API key',), {'validators': [<wtforms.validators.DataRequired object>]})>
+
+ +
+
+default = <UnboundField(BooleanField, ('Default',), {})>
+
+ +
+
+download_limit = <UnboundField(StringField, ('Download limit',), {'default': '20'})>
+
+ +
+
+download_offset = <UnboundField(StringField, ('Download offset',), {'default': '0'})>
+
+ +
+
+name = <UnboundField(StringField, ('Name',), {'validators': [<wtforms.validators.DataRequired object>]})>
+
+ +
+
+url = <UnboundField(StringField, ('URL',), {'validators': [<wtforms.validators.DataRequired object>]})>
+
+ +
+
+user_name = <UnboundField(StringField, ('User name',), {'validators': [<wtforms.validators.DataRequired object>]})>
+
+ +
+ +
+
+

AIPscan.Aggregator.mets_parse_helpers module

+

Collects a number of functions that aid in the retrieval of +information from an AIP METS file.

+
+
+exception AIPscan.Aggregator.mets_parse_helpers.METSError[source]
+

Bases: Exception

+

Exception to signal that we have encountered an error parsing +the METS document.

+
+ +
+
+AIPscan.Aggregator.mets_parse_helpers.get_aip_original_name(mets)[source]
+

Retrieve PREMIS original name from a METSDocument object.

+

If the original name cannot be reliably retrieved from the METS file +a METSError exception is returned to be handled by the caller as +desired.

+
+ +
+
+AIPscan.Aggregator.mets_parse_helpers.parse_mets_with_metsrw(mets_file)[source]
+

Load and Parse the METS.

+

Errors which we encounter at this point will be critical to the +caller and so an exception is returned when we can’t do any better.

+
+ +
+
+

AIPscan.Aggregator.task_helpers module

+

Collects a number of reusable components of tasks.py. Also ensures +the module remains clean and easy to refactor over time.

+
+
+AIPscan.Aggregator.task_helpers.create_numbered_subdirs(timestamp, package_list_number)[source]
+

Check for the existence and create a container folder for our +METS files as required.

+
+ +
+
+AIPscan.Aggregator.task_helpers.download_mets(http_response, package_uuid, subdir)[source]
+

Given a http response containing our METS data, create the path +we want to store our METS at, and then stream the response into a +file.

+
+ +
+
+AIPscan.Aggregator.task_helpers.format_api_url_with_limit_offset(api_url)[source]
+

Format the API URL here to make sure it is as correct as +possible.

+
+ +
+
+AIPscan.Aggregator.task_helpers.get_mets_url(api_url, package_uuid, path_to_mets)[source]
+

Construct a URL from which we can download the METS files that +we are interested in.

+
+ +
+
+AIPscan.Aggregator.task_helpers.get_packages_directory(timestamp)[source]
+

Create a path which we will use to store packages downloaded from +the storage service plus other metadata.

+
+ +
+
+AIPscan.Aggregator.task_helpers.process_package_object(package_obj)[source]
+

Process a package object as retrieve from the storage service +and return a StorageServicePackage type to the caller for further +analysis.

+
+ +
+
+

AIPscan.Aggregator.tasks module

+
+
+exception AIPscan.Aggregator.tasks.TaskError[source]
+

Bases: Exception

+

Exception to call when there is a problem downloading from the +storage service. The exception is known and asks for user +intervention.

+
+ +
+
+AIPscan.Aggregator.tasks.parse_packages_and_load_mets(json_file_path, api_url, timestamp, package_list_no, storage_service_id, fetch_job_id)[source]
+

Parse packages documents from the storage service and initiate +the load mets functions of AIPscan. Results are written to the +database.

+
+ +
+
+AIPscan.Aggregator.tasks.start_mets_task(packageUUID, relativePathToMETS, apiUrl, timestampStr, packageListNo, storageServiceId, fetchJobId)[source]
+

Initiate a get_mets task worker and record the event in the +celery database.

+
+ +
+
+AIPscan.Aggregator.tasks.write_packages_json(count, packages, packages_directory)[source]
+

Write package JSON to disk

+
+ +
+
+

AIPscan.Aggregator.types module

+
+
+exception AIPscan.Aggregator.types.PackageError[source]
+

Bases: Exception

+

There are things we cannot do with the package type unless it +is completed properly. Let the user know.

+
+ +
+
+class AIPscan.Aggregator.types.StorageServicePackage(**kwargs)[source]
+

Bases: object

+

Type that can record information about a storage service package +and provide helpers as to whether or not we should process it.

+
+
+compressed_ext = '.7z'
+
+ +
+
+default_pair_tree = '0000-0000-0000-0000-0000-0000-0000-0000-'
+
+ +
+
+get_relative_path()[source]
+

Return relative path from current_path.

+
+ +
+
+is_aip()[source]
+

Determine whether the package is a AIP

+
+ +
+
+is_deleted()[source]
+

Determine whether the package is a deleted package

+
+ +
+
+is_dip()[source]
+

Determine whether the package is a DIP

+
+ +
+
+is_replica()[source]
+

Determine whether the package is a replica package

+
+ +
+
+is_sip()[source]
+

Determine whether the package is a SIP

+
+ +
+ +
+
+

AIPscan.Aggregator.views module

+
+
+AIPscan.Aggregator.views.delete_fetch_job(id)[source]
+
+ +
+
+AIPscan.Aggregator.views.delete_storage_service(id)[source]
+
+ +
+
+AIPscan.Aggregator.views.edit_storage_service(id)[source]
+
+ +
+
+AIPscan.Aggregator.views.get_mets_task_status(coordinatorid)[source]
+

Get mets task status

+
+ +
+
+AIPscan.Aggregator.views.new_fetch_job(id)[source]
+
+ +
+
+AIPscan.Aggregator.views.new_storage_service()[source]
+
+ +
+
+AIPscan.Aggregator.views.ss()[source]
+
+ +
+
+AIPscan.Aggregator.views.ss_default()[source]
+
+ +
+
+AIPscan.Aggregator.views.storage_service(id)[source]
+
+ +
+
+AIPscan.Aggregator.views.task_status(taskid)[source]
+
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.Aggregator.tests.html b/docs/_build/html/AIPscan.Aggregator.tests.html new file mode 100644 index 00000000..3325794b --- /dev/null +++ b/docs/_build/html/AIPscan.Aggregator.tests.html @@ -0,0 +1,265 @@ + + + + + + + + AIPscan.Aggregator.tests package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan.Aggregator.tests package

+
+

Submodules

+
+
+

AIPscan.Aggregator.tests.test_database_helpers module

+
+
+AIPscan.Aggregator.tests.test_database_helpers.test_collect_agents(app_instance, fixture_path, number_of_unique_agents)[source]
+

Make sure that we retrieve only unique Agents from the METS to +then add to the database. Agents are “repeated” per PREMIS:OBJECT +in METS.

+
+ +
+
+AIPscan.Aggregator.tests.test_database_helpers.test_create_aip(app_instance)[source]
+

Test AIP creation.

+
+ +
+
+AIPscan.Aggregator.tests.test_database_helpers.test_event_creation(app_instance, mocker, fixture_path, event_count, agent_link_multiplier)[source]
+

Make sure that we’re seeing all of the events associated with +an AIP and that they are potentially written to the database okay. +Make sure too that the event_agent_relationship is established.

+
+ +
+
+

AIPscan.Aggregator.tests.test_mets module

+

Tests for METS helper functions.

+
+
+AIPscan.Aggregator.tests.test_mets.test_get_aip_original_name(fixture_path, transfer_name, mets_error)[source]
+

Make sure that we can reliably get original name from the METS +file given we haven’t any mets-reader-writer helpers.

+
+ +
+
+

AIPscan.Aggregator.tests.test_task_helpers module

+
+
+AIPscan.Aggregator.tests.test_task_helpers.packages()[source]
+
+ +
+
+AIPscan.Aggregator.tests.test_task_helpers.test_create_numbered_subdirs(timestamp, package_list_number, result, mocker)[source]
+

Ensure that the logic that we use to create sub-directories for +storing METS is sound.

+
+ +
+
+AIPscan.Aggregator.tests.test_task_helpers.test_format_api_url(url_api_dict, base_url, url_without_api_key, url_with_api_key)[source]
+
+ +
+
+AIPscan.Aggregator.tests.test_task_helpers.test_get_mets_url(api_url, package_uuid, path_to_mets, result)[source]
+

Ensure that the URL for retrieving METS is constructed properly.

+
+ +
+
+AIPscan.Aggregator.tests.test_task_helpers.test_process_package_object(packages, idx, storage_service_package)[source]
+

Test our ability to collect information from a package list +object and parse it into a storage service package type for +further use.

+
+ +
+
+AIPscan.Aggregator.tests.test_task_helpers.test_tz_neutral_dates(input_date, output_date, now_year)[source]
+

Ensure datetime values are handled sensibly across regions.

+
+ +
+
+

AIPscan.Aggregator.tests.test_types module

+
+
+AIPscan.Aggregator.tests.test_types.test_get_relative_path(package, result)[source]
+

Given the current_path check our ability to construct a relative +path within a METS file.

+
+ +
+
+AIPscan.Aggregator.tests.test_types.test_package_ness()[source]
+

Given specific package types check that they evaluate to true +when queried.

+
+ +
+
+AIPscan.Aggregator.tests.test_types.test_storage_service_package_eq()[source]
+

Provide some other equality tests for the type.

+
+ +
+
+AIPscan.Aggregator.tests.test_types.test_storage_service_package_init()[source]
+

Test that the type constructor works as expected.

+
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.Data.html b/docs/_build/html/AIPscan.Data.html new file mode 100644 index 00000000..75c58b9a --- /dev/null +++ b/docs/_build/html/AIPscan.Data.html @@ -0,0 +1,207 @@ + + + + + + + + AIPscan.Data package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan.Data package

+ +
+

Submodules

+
+
+

AIPscan.Data.data module

+
+
+AIPscan.Data.data.aip_overview(storage_service_id, original_files=True)[source]
+

Return a summary overview of all AIPs in a given storage service

+
+ +
+
+AIPscan.Data.data.aip_overview_two(storage_service_id, original_files=True)[source]
+

Return a summary overview of all AIPs in a given storage service

+
+ +
+
+AIPscan.Data.data.derivative_overview(storage_service_id)[source]
+

Return a summary of derivatives across AIPs with a mapping +created between the original format and the preservation copy.

+
+ +
+
+AIPscan.Data.data.largest_files(storage_service_id, file_type=None, limit=20)[source]
+

Return a summary of the largest files in a given Storage Service

+
+
Parameters
+
    +
  • storage_service_id – Storage Service ID.

  • +
  • file_type – Optional filter for type of file to return +(acceptable values are “original” or “preservation”).

  • +
  • limit – Upper limit of number of results to return.

  • +
+
+
Returns
+

“report” dict containing following fields: +report[“StorageName”]: Name of Storage Service queried +report[“Files”]: List of result files ordered desc by size

+
+
+
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.Data.tests.html b/docs/_build/html/AIPscan.Data.tests.html new file mode 100644 index 00000000..7dbf0260 --- /dev/null +++ b/docs/_build/html/AIPscan.Data.tests.html @@ -0,0 +1,168 @@ + + + + + + + + AIPscan.Data.tests package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan.Data.tests package

+
+

Submodules

+
+
+

AIPscan.Data.tests.test_largest_files module

+
+
+AIPscan.Data.tests.test_largest_files.test_largest_files(app_instance, mocker, file_data, file_count)[source]
+

Test that return value conforms to expected structure.

+
+ +
+
+AIPscan.Data.tests.test_largest_files.test_largest_files_elements(app_instance, mocker, test_file, has_format_version, has_puid)[source]
+

Test that returned file data matches expected values.

+
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.Home.html b/docs/_build/html/AIPscan.Home.html new file mode 100644 index 00000000..62db4440 --- /dev/null +++ b/docs/_build/html/AIPscan.Home.html @@ -0,0 +1,154 @@ + + + + + + + + AIPscan.Home package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan.Home package

+
+

Submodules

+
+
+

AIPscan.Home.views module

+
+
+AIPscan.Home.views.index()[source]
+

Define handling for application’s / route.

+
+ +
+
+

Module contents

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.Reporter.html b/docs/_build/html/AIPscan.Reporter.html new file mode 100644 index 00000000..5e167ece --- /dev/null +++ b/docs/_build/html/AIPscan.Reporter.html @@ -0,0 +1,258 @@ + + + + + + + + AIPscan.Reporter package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan.Reporter package

+
+

Submodules

+
+
+

AIPscan.Reporter.helpers module

+

Code shared across reporting modules but not outside of reporting.

+
+
+AIPscan.Reporter.helpers.translate_headers(headers)[source]
+

Translate headers from something machine readable to something +more user friendly and translatable.

+
+ +
+
+

AIPscan.Reporter.report_aip_contents module

+
+
+AIPscan.Reporter.report_aip_contents.aip_contents()[source]
+

Return AIP contents organized by format.

+
+ +
+
+

AIPscan.Reporter.report_formats_count module

+

Report formats count consists of the tabular report, plot, and +chart which describe the file formats present across the AIPs in a +storage service with AIPs filtered by date range.

+
+
+AIPscan.Reporter.report_formats_count.chart_formats_count()[source]
+

Report (pie chart) on all file formats and their counts and size +on disk across all AIPs in the storage service.

+
+ +
+
+AIPscan.Reporter.report_formats_count.plot_formats_count()[source]
+

Report (scatter) on all file formats and their counts and size on +disk across all AIPs in the storage service.

+
+ +
+
+AIPscan.Reporter.report_formats_count.report_formats_count()[source]
+

Report (tabular) on all file formats and their counts and size on +disk across all AIPs in the storage service.

+
+ +
+
+

AIPscan.Reporter.report_largest_files module

+
+
+AIPscan.Reporter.report_largest_files.largest_files()[source]
+

Return largest files.

+
+ +
+
+

AIPscan.Reporter.report_originals_with_derivatives module

+

Report on original copies that have a preservation derivative and +the different file formats associated with both.

+
+
+AIPscan.Reporter.report_originals_with_derivatives.original_derivatives()[source]
+

Return a mapping between original files and derivatives if they +exist.

+
+ +
+
+

AIPscan.Reporter.views module

+

Views contains the primary routes for navigation around AIPscan’s +Reporter module. Reports themselves as siphoned off into separate module +files with singular responsibility for a report.

+
+
+AIPscan.Reporter.views.reports()[source]
+

Reports returns a standard page in AIPscan that lists the +in-built reports available to the caller.

+
+ +
+
+AIPscan.Reporter.views.view_aip(aip_id)[source]
+

View aip returns a standard page in AIPscan that provides a more +detailed view of a specific AIP given an AIPs ID.

+
+ +
+
+AIPscan.Reporter.views.view_aips(storage_service_id=0)[source]
+

View aips returns a standard page in AIPscan that provides an +overview of the AIPs in a given storage service.

+
+ +
+
+AIPscan.Reporter.views.view_file(file_id)[source]
+

File page displays Object and Event metadata for file

+
+ +
+
+

Module contents

+

Initialize components which need to be shared across the report +module, notably too the Blueprint itself.

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.User.html b/docs/_build/html/AIPscan.User.html new file mode 100644 index 00000000..c8e33bc3 --- /dev/null +++ b/docs/_build/html/AIPscan.User.html @@ -0,0 +1,175 @@ + + + + + + + + AIPscan.User package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan.User package

+
+

Submodules

+
+
+

AIPscan.User.forms module

+
+
+class AIPscan.User.forms.LoginForm(*args, **kwargs)[source]
+

Bases: flask_wtf.form.FlaskForm

+
+
+password = <UnboundField(PasswordField, ('Password',), {'validators': [<wtforms.validators.DataRequired object>]})>
+
+ +
+
+remember_me = <UnboundField(BooleanField, ('Remember Me',), {})>
+
+ +
+
+submit = <UnboundField(SubmitField, ('Sign In',), {})>
+
+ +
+
+username = <UnboundField(StringField, ('Username',), {'validators': [<wtforms.validators.DataRequired object>]})>
+
+ +
+ +
+
+

AIPscan.User.views module

+
+
+

Module contents

+
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/AIPscan.html b/docs/_build/html/AIPscan.html new file mode 100644 index 00000000..c34754a1 --- /dev/null +++ b/docs/_build/html/AIPscan.html @@ -0,0 +1,692 @@ + + + + + + + + AIPscan package — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan package

+
+

Subpackages

+
+ +
+
+
+

Submodules

+
+
+

AIPscan.celery module

+

This module contains code related to Celery configuration.

+
+
+AIPscan.celery.configure_celery(app)[source]
+

Add Flask app context to celery.Task.

+
+ +
+
+

AIPscan.conftest module

+

This module defines shared AIPscan pytest fixtures.

+
+
+AIPscan.conftest.app_instance()[source]
+

Pytest fixture that returns an instance of our application.

+

This fixture provides a Flask application context for tests using +AIPscan’s test configuration.

+

This pattern can be extended in additional fixtures to, e.g. load +state to the test database from a fixture as needed for tests.

+
+ +
+
+

AIPscan.extensions module

+

This module contains code related to Flask extensions.

+

The Celery instance that is initialized here is lacking application +context, which will be provided via AIPscan.celery’s configure_celery +function.

+
+
+

AIPscan.helpers module

+
+
+AIPscan.helpers.get_human_readable_file_size(size, precision=2)[source]
+
+ +
+
+

AIPscan.models module

+
+
+class AIPscan.models.AIP(uuid, transfer_name, create_date, storage_service_id, fetch_job_id)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+create_date
+
+ +
+
+fetch_job_id
+
+ +
+
+files
+
+ +
+
+id
+
+ +
+
+property original_file_count
+
+ +
+
+property preservation_file_count
+
+ +
+
+storage_service_id
+
+ +
+
+transfer_name
+
+ +
+
+uuid
+
+ +
+ +
+
+class AIPscan.models.Agent(linking_type_value, agent_type, agent_value)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+Event
+
+ +
+
+agent_type
+
+ +
+
+agent_value
+
+ +
+
+id
+
+ +
+
+linking_type_value
+
+ +
+ +
+
+class AIPscan.models.Event(type, uuid, date, detail, outcome, outcome_detail, file_id)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+date
+
+ +
+
+detail
+
+ +
+
+event_agents
+
+ +
+
+file
+
+ +
+
+file_id
+
+ +
+
+id
+
+ +
+
+outcome
+
+ +
+
+outcome_detail
+
+ +
+
+type
+
+ +
+
+uuid
+
+ +
+ +
+
+class AIPscan.models.FetchJob(total_packages, total_aips, total_deleted_aips, download_start, download_end, download_directory, storage_service_id)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+aips
+
+ +
+
+download_directory
+
+ +
+
+download_end
+
+ +
+
+download_start
+
+ +
+
+id
+
+ +
+
+storage_service
+
+ +
+
+storage_service_id
+
+ +
+
+total_aips
+
+ +
+
+total_deleted_aips
+
+ +
+
+total_dips
+
+ +
+
+total_packages
+
+ +
+
+total_replicas
+
+ +
+
+total_sips
+
+ +
+ +
+
+class AIPscan.models.File(name, filepath, uuid, size, date_created, file_format, checksum_type, checksum_value, aip_id, file_type=<FileType.original: 'original'>, format_version=None, puid=None, original_file_id=None)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+aip
+
+ +
+
+aip_id
+
+ +
+
+checksum_type
+
+ +
+
+checksum_value
+
+ +
+
+date_created
+
+ +
+
+derivatives
+
+ +
+
+events
+
+ +
+
+file_format
+
+ +
+
+file_type
+
+ +
+
+filepath
+
+ +
+
+format_version
+
+ +
+
+id
+
+ +
+
+name
+
+ +
+
+original_file
+
+ +
+
+original_file_id
+
+ +
+
+puid
+
+ +
+
+size
+
+ +
+
+uuid
+
+ +
+ +
+
+class AIPscan.models.FileType(value)[source]
+

Bases: enum.Enum

+

An enumeration.

+
+
+original = 'original'
+
+ +
+
+preservation = 'preservation'
+
+ +
+ +
+
+class AIPscan.models.StorageService(name, url, user_name, api_key, download_limit, download_offset, default)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+api_key
+
+ +
+
+default
+
+ +
+
+download_limit
+
+ +
+
+download_offset
+
+ +
+
+fetch_jobs
+
+ +
+
+id
+
+ +
+
+name
+
+ +
+
+url
+
+ +
+
+user_name
+
+ +
+ +
+
+class AIPscan.models.get_mets_tasks(**kwargs)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+get_mets_task_id
+
+ +
+
+package_uuid
+
+ +
+
+status
+
+ +
+
+workflow_coordinator_id
+
+ +
+ +
+
+class AIPscan.models.package_tasks(**kwargs)[source]
+

Bases: sqlalchemy.ext.declarative.api.Model

+
+
+package_task_id
+
+ +
+
+workflow_coordinator_id
+
+ +
+ +
+
+

AIPscan.worker module

+

This module defines and initalizes a Celery worker.

+

Since Celery workers are run separately from the Flask application (for +example via a systemd service), we use our Application Factory function +to provide application context.

+
+
+

Module contents

+
+
+AIPscan.create_app(config_name='default')[source]
+

Flask app factory, returns app instance.

+
+ +
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan.html b/docs/_build/html/_modules/AIPscan.html new file mode 100644 index 00000000..e82695ef --- /dev/null +++ b/docs/_build/html/_modules/AIPscan.html @@ -0,0 +1,147 @@ + + + + + + + + AIPscan — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan

+# -*- coding: utf-8 -*-
+
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+
+from AIPscan.celery import configure_celery
+from config import CONFIGS
+
+db = SQLAlchemy()
+
+
+
[docs]def create_app(config_name="default"): + """Flask app factory, returns app instance.""" + app = Flask(__name__) + + app.config.from_object(CONFIGS[config_name]) + + with app.app_context(): + + from AIPscan.Aggregator.views import aggregator + from AIPscan.Reporter.views import reporter + from AIPscan.User.views import user + from AIPscan.API.views import api + from AIPscan.Home.views import home + + app.register_blueprint(aggregator, url_prefix="/aggregator") + app.register_blueprint(reporter, url_prefix="/reporter") + app.register_blueprint(user, url_prefix="/user") + app.register_blueprint(api) + app.register_blueprint(home) + + db.init_app(app) + configure_celery(app) + + db.create_all() + + return app
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/API/namespace_data.html b/docs/_build/html/_modules/AIPscan/API/namespace_data.html new file mode 100644 index 00000000..44405a4e --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/API/namespace_data.html @@ -0,0 +1,221 @@ + + + + + + + + AIPscan.API.namespace_data — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.API.namespace_data

+# -*- coding: utf-8 -*-
+
+"""Data namespace
+
+Return 'unkempt' data from AIPscan so that it can be filtered, cut, and
+remixed as the caller desires. Data is 'unkempt' not raw. No data is
+raw. No data is without bias.
+"""
+from distutils.util import strtobool
+
+from flask import request
+from flask_restx import Namespace, Resource
+from AIPscan.Data import data
+
+api = Namespace("data", description="Retrieve data from AIPscan to shape as you desire")
+
+
+
[docs]def parse_bool(val, default=True): + try: + return bool(strtobool(val)) + except (ValueError, AttributeError): + return default
+ + +
[docs]@api.route("/aip-overview/<storage_service_id>") +class FMTList(Resource): +
[docs] @api.doc( + "list_formats", + params={ + "original_files": { + "description": "Return data for original files or copies", + "in": "query", + "type": "bool", + } + }, + ) + def get(self, storage_service_id): + """AIP overview One""" + try: + original_files = parse_bool(request.args.get("original_files"), True) + except TypeError: + pass + aip_data = data.aip_overview( + storage_service_id=storage_service_id, original_files=original_files + ) + return aip_data
+ + +
[docs]@api.route("/fmt-overview/<storage_service_id>") +class AIPList(Resource): +
[docs] @api.doc( + "list_formats", + params={ + "original_files": { + "description": "Return data for original files or copies", + "in": "query", + "type": "bool", + } + }, + ) + def get(self, storage_service_id): + """AIP overview two""" + try: + original_files = parse_bool(request.args.get("original_files"), True) + except TypeError: + pass + aip_data = data.aip_overview_two( + storage_service_id=storage_service_id, original_files=original_files + ) + return aip_data
+ + +
[docs]@api.route("/derivative-overview/<storage_service_id>") +class DerivativeList(Resource): +
[docs] @api.doc("list_aips") + def get(self, storage_service_id): + """AIP overview two""" + aip_data = data.derivative_overview(storage_service_id=storage_service_id) + return aip_data
+ + +
[docs]@api.route("/largest-files/<storage_service_id>") +class LargestFileList(Resource): +
[docs] @api.doc( + "list_formats", + params={ + "file_type": { + "description": "Optional file type filter (original or preservation)", + "in": "query", + "type": "str", + }, + "limit": { + "description": "Number of results to return (default is 20)", + "in": "query", + "type": "int", + }, + }, + ) + def get(self, storage_service_id, file_type=None, limit=20): + """Largest files""" + file_type = request.args.get("file_type", None) + try: + limit = int(request.args.get("limit", 20)) + except ValueError: + pass + file_data = data.largest_files( + storage_service_id=storage_service_id, file_type=file_type, limit=limit + ) + return file_data
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/API/namespace_infos.html b/docs/_build/html/_modules/AIPscan/API/namespace_infos.html new file mode 100644 index 00000000..63e0bbaf --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/API/namespace_infos.html @@ -0,0 +1,147 @@ + + + + + + + + AIPscan.API.namespace_infos — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.API.namespace_infos

+# -*- coding: utf-8 -*-
+
+"""Infos namespace
+
+At the moment, this is just a demonstration endpoint to show how
+a namespace can be created and used. We only use it to return version
+from the application itself.
+"""
+
+from flask_restx import Namespace, Resource, fields
+
+api = Namespace(
+    "infos", description="Additional metadata associated with the application"
+)
+
+version = api.model(
+    "Version",
+    {
+        "version": fields.String(
+            required=True, description="The current version of AIPscan"
+        )
+    },
+)
+
+# TODO: Get this from the application wherever we decide to put it...
+APPVERSION = {"version": "0.1"}
+
+
+
[docs]@api.route("/version") +class Version(Resource): +
[docs] @api.doc("aipscan_version") + @api.marshal_list_with(version) + def get(self): + """Get the application version""" + return APPVERSION
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/celery_helpers.html b/docs/_build/html/_modules/AIPscan/Aggregator/celery_helpers.html new file mode 100644 index 00000000..406bbb6d --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/celery_helpers.html @@ -0,0 +1,126 @@ + + + + + + + + AIPscan.Aggregator.celery_helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.celery_helpers

+# -*- coding: utf-8 -*-
+
+from AIPscan import db
+from AIPscan.models import package_tasks
+
+
+
[docs]def write_celery_update(package_lists_task, workflow_coordinator): + # Write status update to celery model. + package_task = package_tasks( + package_task_id=package_lists_task.id, + workflow_coordinator_id=workflow_coordinator.request.id, + ) + db.session.add(package_task) + db.session.commit()
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/database_helpers.html b/docs/_build/html/_modules/AIPscan/Aggregator/database_helpers.html new file mode 100644 index 00000000..c7906dce --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/database_helpers.html @@ -0,0 +1,403 @@ + + + + + + + + AIPscan.Aggregator.database_helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.database_helpers

+# -*- coding: utf-8 -*-
+
+"""Functions to help us tease apart a METS file and write to the
+database.
+"""
+
+from celery.utils.log import get_task_logger
+
+from AIPscan import db
+from AIPscan.models import AIP, File, FileType, Event, Agent, EventAgent
+
+from AIPscan.Aggregator.task_helpers import _tz_neutral_date
+from AIPscan.Aggregator import tasks
+
+logger = get_task_logger(__name__)
+
+
+ORIGINAL_OBJECT = "original"
+PRESERVATION_OBJECT = "preservation"
+
+
+def _extract_event_detail(premis_event, file_id):
+    """Extract the detail from the event and write a new event object
+    to the database"""
+    event_type = premis_event.event_type
+    event_uuid = premis_event.event_identifier_value
+    event_date = _tz_neutral_date(premis_event.event_date_time)
+    # We have a strange issue with this logged: https://github.com/archivematica/Issues/issues/743
+    event_detail, event_outcome, event_outcome_detail = None, None, None
+    if not isinstance(premis_event.event_detail, tuple):
+        event_detail = premis_event.event_detail
+    if not isinstance(premis_event.event_outcome, tuple):
+        event_outcome = premis_event.event_outcome
+    if not isinstance(premis_event.event_outcome_detail_note, tuple):
+        event_outcome_detail = premis_event.event_outcome_detail_note
+    event = Event(
+        type=event_type,
+        uuid=event_uuid,
+        date=event_date,
+        detail=event_detail,
+        outcome=event_outcome,
+        outcome_detail=event_outcome_detail,
+        file_id=file_id,
+    )
+    return event
+
+
+def _create_agent_type_id(identifier_type, identifier_value):
+    """Create a key-pair string for the linking_type_value in the db.
+    """
+    return "{}-{}".format(identifier_type, identifier_value)
+
+
+def _create_event_agent_relationship(event_id, agent_identifier):
+    """Generator object helper for looping through an event's agents and
+    returning the event-agent IDs.
+    """
+    for agent_ in agent_identifier:
+        id_ = _create_agent_type_id(
+            agent_.linking_agent_identifier_type, agent_.linking_agent_identifier_value
+        )
+        existing_agent = Agent.query.filter_by(linking_type_value=id_).first()
+        event_relationship = EventAgent.insert().values(
+            event_id=event_id, agent_id=existing_agent.id
+        )
+        yield event_relationship
+
+
+
[docs]def create_event_objects(fs_entry, file_id): + """Add information about PREMIS Events associated with file to database + + :param fs_entry: mets-reader-writer FSEntry object + :param file_id: File ID + """ + for premis_event in fs_entry.get_premis_events(): + event = _extract_event_detail(premis_event, file_id) + db.session.add(event) + db.session.commit() + + for event_relationship in _create_event_agent_relationship( + event.id, premis_event.linking_agent_identifier + ): + db.session.execute(event_relationship) + db.session.commit()
+ + +def _extract_agent_detail(agent): + """Pull the agent information from the agent record and return an + agent object ready to insert into the database. + """ + linking_type_value = agent[0] + agent_type = agent[1] + agent_value = agent[2] + return Agent( + linking_type_value=linking_type_value, + agent_type=agent_type, + agent_value=agent_value, + ) + + +
[docs]def create_agent_objects(unique_agents): + """Add our agents to the database. The list is already the + equivalent of a set by the time it reaches here and so we don't + need to perform any de-duplication. + """ + for agent in unique_agents: + agent_obj = _extract_agent_detail(agent) + exists = Agent.query.filter_by( + linking_type_value=agent_obj.linking_type_value, + agent_type=agent_obj.agent_type, + agent_value=agent_obj.agent_value, + ).count() + if exists: + continue + logger.info("Adding: %s", agent_obj) + db.session.add(agent_obj) + db.session.commit()
+ + +def _get_unique_agents(all_agents, agents_list): + """Return unique agents associated with a file in the AIP. Returns + a list of tuples organized by (linking_type_value, type, + value), e.g. (Archivematica user pk 4, Archivematica User, Eric). + Linking type and value become one because only together are these + two considered useful, they just happen to exist as two elements + in the PREMIS dictionary. + """ + agents = [] + linking_type_value = "" + for agent in all_agents: + linking_type_value = _create_agent_type_id( + agent.agent_identifier[0].agent_identifier_type, + agent.agent_identifier[0].agent_identifier_value, + ) + agent_tuple = (linking_type_value, agent.type, agent.name) + if agent_tuple not in agents_list: + agents.append(agent_tuple) + return agents + + +
[docs]def create_aip_object( + package_uuid, transfer_name, create_date, storage_service_id, fetch_job_id +): + """Create an AIP object and save it to the database.""" + aip = AIP( + uuid=package_uuid, + transfer_name=transfer_name, + create_date=_tz_neutral_date(create_date), + storage_service_id=storage_service_id, + fetch_job_id=fetch_job_id, + ) + db.session.add(aip) + db.session.commit() + return aip
+ + +def _get_file_properties(fs_entry): + """Retrieve file properties from FSEntry + + :param fs_entry: mets-reader-writer FSEntry object + + :returns: Dict of file properties + """ + file_info = { + "uuid": fs_entry.file_uuid, + "name": fs_entry.label, + "filepath": fs_entry.path, + "size": None, + "date_created": None, + "puid": None, + "file_format": None, + "format_version": None, + "checksum_type": None, + "checksum_value": None, + "related_uuid": None, + } + + try: + for premis_object in fs_entry.get_premis_objects(): + file_info["size"] = premis_object.size + key_alias = premis_object.format_registry_key + file_info["date_created"] = _tz_neutral_date( + premis_object.date_created_by_application + ) + if not isinstance(key_alias, tuple): + file_info["puid"] = key_alias + file_info["file_format"] = premis_object.format_name + version_alias = premis_object.format_version + if not isinstance(version_alias, tuple): + file_info["format_version"] = version_alias + file_info["checksum_type"] = premis_object.message_digest_algorithm + file_info["checksum_value"] = premis_object.message_digest + related_uuid_alias = premis_object.related_object_identifier_value + if not isinstance(related_uuid_alias, tuple): + file_info["related_uuid"] = related_uuid_alias + except AttributeError: + # File error/warning to log. Obviously this format may + # be incorrect so it is our best guess. + file_info["file_format"] = "ISO Disk Image File" + file_info["puid"] = "fmt/468" + + return file_info + + +def _add_normalization_date(file_id): + """Add normalization date from PREMIS creation Event to preservation file + + :param file_id: File ID + """ + file_ = File.query.get(file_id) + creation_event = Event.query.filter_by(file_id=file_.id, type="creation").first() + if creation_event is not None: + file_.date_created = creation_event.date + db.session.commit() + + +def _add_file(file_type, fs_entry, aip_id): + """Add file to database + + :param file_type: models.FileType enum + :param fs_entry: mets-reader-writer FSEntry object + :param aip_id: AIP ID + """ + file_info = _get_file_properties(fs_entry) + + original_file_id = None + if file_type is FileType.preservation: + original_file = File.query.filter_by(uuid=file_info["related_uuid"]).first() + original_file_id = original_file.id + + new_file = File( + name=file_info.get("name"), + filepath=file_info.get("filepath"), + uuid=file_info.get("uuid"), + file_type=file_type, + size=file_info.get("size"), + date_created=file_info.get("date_created"), + puid=file_info.get("puid"), + file_format=file_info.get("file_format"), + format_version=file_info.get("format_version"), + checksum_type=file_info.get("checksum_type"), + checksum_value=file_info.get("checksum_value"), + original_file_id=original_file_id, + aip_id=aip_id, + ) + + logger.debug("Adding file %s %s", new_file.name, aip_id) + + db.session.add(new_file) + db.session.commit() + + create_event_objects(fs_entry, new_file.id) + + if file_type == FileType.preservation: + _add_normalization_date(new_file.id) + + +
[docs]def collect_mets_agents(mets): + """Collect all of the unique agents in the METS file to write to the + database. + """ + agents = [] + for aip_file in mets.all_files(): + if aip_file.use != ORIGINAL_OBJECT and aip_file.use != PRESERVATION_OBJECT: + continue + agents = agents + _get_unique_agents(aip_file.get_premis_agents(), agents) + logger.info("Total agents: %d", len(agents)) + return agents
+ + +
[docs]def process_aip_data(aip, mets): + """Populate database with information needed for reporting from METS file + + :param aip: AIP object + :param mets: mets-reader-writer METSDocument object + """ + tasks.get_mets.update_state(state="IN PROGRESS") + + create_agent_objects(collect_mets_agents(mets)) + + all_files = mets.all_files() + + # Parse the original files first so that they are available as foreign keys + # when we parse preservation and derivative files. + original_files = [file_ for file_ in all_files if file_.use == "original"] + for file_ in original_files: + _add_file(FileType.original, file_, aip.id) + + preservation_files = [file_ for file_ in all_files if file_.use == "preservation"] + for file_ in preservation_files: + _add_file(FileType.preservation, file_, aip.id)
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/forms.html b/docs/_build/html/_modules/AIPscan/Aggregator/forms.html new file mode 100644 index 00000000..13477639 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/forms.html @@ -0,0 +1,127 @@ + + + + + + + + AIPscan.Aggregator.forms — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.forms

+# -*- coding: utf-8 -*-
+
+from flask_wtf import FlaskForm
+from wtforms import StringField, BooleanField
+from wtforms.validators import DataRequired
+
+
+
[docs]class StorageServiceForm(FlaskForm): + name = StringField("Name", validators=[DataRequired()]) + url = StringField("URL", validators=[DataRequired()]) + user_name = StringField("User name", validators=[DataRequired()]) + api_key = StringField("API key", validators=[DataRequired()]) + download_limit = StringField("Download limit", default="20") + download_offset = StringField("Download offset", default="0") + default = BooleanField("Default")
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/mets_parse_helpers.html b/docs/_build/html/_modules/AIPscan/Aggregator/mets_parse_helpers.html new file mode 100644 index 00000000..650d719d --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/mets_parse_helpers.html @@ -0,0 +1,216 @@ + + + + + + + + AIPscan.Aggregator.mets_parse_helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.mets_parse_helpers

+# -*- coding: utf-8 -*-
+
+"""Collects a number of functions that aid in the retrieval of
+information from an AIP METS file.
+"""
+import lxml
+import requests
+
+import metsrw
+
+from AIPscan.Aggregator.task_helpers import (
+    create_numbered_subdirs,
+    download_mets,
+    get_mets_url,
+)
+
+
+
[docs]class METSError(Exception): + """Exception to signal that we have encountered an error parsing + the METS document. + """
+ + +
[docs]def parse_mets_with_metsrw(mets_file): + """Load and Parse the METS. + + Errors which we encounter at this point will be critical to the + caller and so an exception is returned when we can't do any better. + """ + try: + mets = metsrw.METSDocument.fromfile(mets_file) + except AttributeError as err: + # See archivematica/issues#1129 where METSRW expects a certain + # METS structure but Archivematica has written it incorrectly. + err = "{}: {}".format(err, mets_file) + raise METSError("Error parsing METS: Cannot return a METSDocument") + except lxml.etree.Error as err: + # We have another undetermined storage service error, e.g. the + # package no longer exists on the server, or another download + # error. + err = "Error parsing METS: {}: {}".format(err, mets_file) + raise METSError(err) + return mets
+ + +
[docs]def get_aip_original_name(mets): + """Retrieve PREMIS original name from a METSDocument object. + + If the original name cannot be reliably retrieved from the METS file + a METSError exception is returned to be handled by the caller as + desired. + """ + + # Negated as we're going to want to remove this length of values. + NAMESUFFIX = -len("-00000000-0000-0000-0000-000000000000") + + # The transfer directory prefix is a directory prefix that can also + # exist in a dmdSec intellectual entity and we want to identify and + # ignore those. + TRANSFER_DIR_PREFIX = "%transferDirectory%" + + NAMESPACES = {u"premis": u"http://www.loc.gov/premis/v3"} + ELEM_ORIGINAL_NAME_PATTERN = ".//premis:originalName" + + original_name = "" + for fsentry in mets.all_files(): + for dmdsec in fsentry.dmdsecs: + dmd_element = dmdsec.serialize() + full_name = dmd_element.find( + ELEM_ORIGINAL_NAME_PATTERN, namespaces=NAMESPACES + ) + if full_name is not None and full_name.text.startswith(TRANSFER_DIR_PREFIX): + # We don't want this value, it will usually represent an + # directory entity. + continue + try: + original_name = full_name.text[:NAMESUFFIX] + except AttributeError: + continue + + # There should be a transfer name in every METS. + if original_name == "": + raise METSError("Cannot locate transfer name in METS") + + return original_name
+ + +def _download_mets( + api_url, package_uuid, relative_path_to_mets, timestamp, package_list_no +): + """Download METS from the storage service.""" + + # Request the METS file. + mets_response = requests.get( + get_mets_url(api_url, package_uuid, relative_path_to_mets) + ) + + # Create a directory to download the METS to. + numbered_subdir = create_numbered_subdirs(timestamp, package_list_no) + + # Output METS to a convenient location to later be parsed. + download_file = download_mets(mets_response, package_uuid, numbered_subdir) + + return download_file +
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/task_helpers.html b/docs/_build/html/_modules/AIPscan/Aggregator/task_helpers.html new file mode 100644 index 00000000..764f6142 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/task_helpers.html @@ -0,0 +1,249 @@ + + + + + + + + AIPscan.Aggregator.task_helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.task_helpers

+# -*- coding: utf-8 -*-
+
+"""Collects a number of reusable components of tasks.py. Also ensures
+the module remains clean and easy to refactor over time.
+"""
+from datetime import datetime
+import os
+
+from dateutil.parser import parse, ParserError
+
+from AIPscan.Aggregator.types import StorageServicePackage
+
+
+
[docs]def format_api_url_with_limit_offset(api_url): + """Format the API URL here to make sure it is as correct as + possible. + """ + base_url = api_url.get("baseUrl", "").rstrip("/") + limit = int(api_url.get("limit", "")) + offset = api_url.get("offset", "") + user_name = api_url.get("userName") + api_key = api_url.get("apiKey", "") + request_url_without_api_key = "{}/api/v2/file/?limit={}&offset={}".format( + base_url, limit, offset + ) + request_url = "{}&username={}&api_key={}".format( + request_url_without_api_key, user_name, api_key + ) + return base_url, request_url_without_api_key, request_url
+ + +
[docs]def get_packages_directory(timestamp): + """Create a path which we will use to store packages downloaded from + the storage service plus other metadata. + """ + return os.path.join("AIPscan", "Aggregator", "downloads", timestamp, "packages")
+ + +
[docs]def process_package_object(package_obj): + """Process a package object as retrieve from the storage service + and return a StorageServicePackage type to the caller for further + analysis. + """ + package = StorageServicePackage() + + STATUS = "status" + TYPE = "package_type" + REPL = "replicated_package" + CURRENT_PATH = "current_path" + UUID = "uuid" + + PKG_AIP = "AIP" + PKG_SIP = "transfer" + PKG_DIP = "DIP" + PKG_DEL = "DELETED" + + # Accumulate state. The package object should be able to evaluate + # itself accordingly. + if package_obj[TYPE] == PKG_AIP: + package.aip = True + if package_obj.get(TYPE) == PKG_DIP: + package.dip = True + if package_obj.get(TYPE) == PKG_SIP: + package.sip = True + if package_obj.get(STATUS) == PKG_DEL: + package.deleted = True + if package_obj.get(REPL) is not None: + package.replica = True + + package.uuid = package_obj.get(UUID) + package.current_path = package_obj.get(CURRENT_PATH) + + return package
+ + +def _tz_neutral_date(date): + """Convert inconsistent dates consistently. Dates are round-tripped + back to a Python datetime object as anticipated by the database. + Where a date is unknown or can't be parsed, we return the UNIX EPOCH + in lieu of another sensible value. + """ + date_time_pattern = "%Y-%m-%dT%H:%M:%S" + EPOCH = datetime.strptime("1970-01-01T00:00:01", date_time_pattern) + try: + date = parse(date) + date = date.strftime(date_time_pattern) + date = datetime.strptime(date, date_time_pattern) + except ParserError: + date = EPOCH + return date + + +
[docs]def get_mets_url(api_url, package_uuid, path_to_mets): + """Construct a URL from which we can download the METS files that + we are interested in. + """ + am_url = "baseUrl" + user_name = "userName" + api_key = "apiKey" + + mets_url = "{}/api/v2/file/{}/extract_file/?relative_path_to_file={}&username={}&api_key={}".format( + api_url[am_url].rstrip("/"), + package_uuid, + path_to_mets, + api_url[user_name], + api_url[api_key], + ) + return mets_url
+ + +
[docs]def create_numbered_subdirs(timestamp, package_list_number): + """Check for the existence and create a container folder for our + METS files as required. + """ + AGGREGATOR_DOWNLOADS = os.path.join("AIPscan", "Aggregator", "downloads") + METS_FOLDER = "mets" + + # Create a package list numbered subdirectory if it doesn't exist. + numbered_subdir = os.path.join( + AGGREGATOR_DOWNLOADS, timestamp, METS_FOLDER, str(package_list_number) + ) + if not os.path.exists(numbered_subdir): + os.makedirs(numbered_subdir) + + return numbered_subdir
+ + +
[docs]def download_mets(http_response, package_uuid, subdir): + """Given a http response containing our METS data, create the path + we want to store our METS at, and then stream the response into a + file. + """ + mets_file = "METS.{}.xml".format(package_uuid) + download_file = os.path.join(subdir, mets_file) + with open(download_file, "wb") as file: + file.write(http_response.content) + return download_file
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/tasks.html b/docs/_build/html/_modules/AIPscan/Aggregator/tasks.html new file mode 100644 index 00000000..4625aba5 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/tasks.html @@ -0,0 +1,419 @@ + + + + + + + + AIPscan.Aggregator.tasks — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.tasks

+# -*- coding: utf-8 -*-
+
+import json
+import os
+import requests
+
+from celery.utils.log import get_task_logger
+
+from AIPscan import db
+from AIPscan.extensions import celery
+from AIPscan.models import (
+    FetchJob,
+    # Custom celery Models.
+    get_mets_tasks,
+)
+
+from AIPscan.Aggregator.celery_helpers import write_celery_update
+from AIPscan.Aggregator import database_helpers
+
+from AIPscan.Aggregator.mets_parse_helpers import (
+    _download_mets,
+    METSError,
+    get_aip_original_name,
+    parse_mets_with_metsrw,
+)
+
+from AIPscan.Aggregator.task_helpers import (
+    process_package_object,
+    format_api_url_with_limit_offset,
+)
+
+logger = get_task_logger(__name__)
+
+
+
[docs]class TaskError(Exception): + """Exception to call when there is a problem downloading from the + storage service. The exception is known and asks for user + intervention. + """
+ + +
[docs]def write_packages_json(count, packages, packages_directory): + """Write package JSON to disk""" + json_download_file = os.path.join( + packages_directory, "packages{}.json".format(count) + ) + logger.info("Packages file is downloaded to '%s'", json_download_file) + try: + with open(json_download_file, "w") as json_file: + json.dump(packages, json_file, indent=4) + except json.JSONDecodeError: + logger.error("Cannot decode JSON from %s", json_download_file)
+ + +
[docs]def start_mets_task( + packageUUID, + relativePathToMETS, + apiUrl, + timestampStr, + packageListNo, + storageServiceId, + fetchJobId, +): + """Initiate a get_mets task worker and record the event in the + celery database. + """ + # call worker to download and parse METS File + get_mets_task = get_mets.delay( + packageUUID, + relativePathToMETS, + apiUrl, + timestampStr, + packageListNo, + storageServiceId, + fetchJobId, + ) + mets_task = get_mets_tasks( + get_mets_task_id=get_mets_task.id, + workflow_coordinator_id=workflow_coordinator.request.id, + package_uuid=packageUUID, + status=None, + ) + db.session.add(mets_task) + db.session.commit()
+ + +
[docs]def parse_packages_and_load_mets( + json_file_path, + api_url, + timestamp, + package_list_no, + storage_service_id, + fetch_job_id, +): + """Parse packages documents from the storage service and initiate + the load mets functions of AIPscan. Results are written to the + database. + """ + OBJECTS = "objects" + packages = [] + with open(json_file_path, "r") as packagesJson: + package_list = json.load(packagesJson) + for package_obj in package_list.get(OBJECTS, []): + package = process_package_object(package_obj) + packages.append(package) + if not package.is_aip(): + continue + start_mets_task( + package.uuid, + package.get_relative_path(), + api_url, + timestamp, + package_list_no, + storage_service_id, + fetch_job_id, + ) + return packages
+ + +@celery.task(bind=True) +def workflow_coordinator( + self, api_url, timestamp, storage_service_id, fetch_job_id, packages_directory +): + + logger.info("Packages directory is: %s", packages_directory) + + # Send package list request to a worker. + package_lists_task = package_lists_request.delay( + api_url, timestamp, packages_directory + ) + + write_celery_update(package_lists_task, workflow_coordinator) + + # Wait for package lists task to finish downloading all package + # lists. + task = package_lists_request.AsyncResult(package_lists_task.id, app=celery) + while True: + if (task.state == "SUCCESS") or (task.state == "FAILURE"): + break + + if isinstance(package_lists_task.info, TaskError): + # Re-raise. + raise (package_lists_task.info) + + total_package_lists = package_lists_task.info["totalPackageLists"] + + all_packages = [] + for package_list_no in range(1, total_package_lists + 1): + json_file_path = os.path.join( + packages_directory, "packages{}.json".format(package_list_no) + ) + # Process packages and create a new worker to download and parse + # each METS separately. + packages = parse_packages_and_load_mets( + json_file_path, + api_url, + timestamp, + package_list_no, + storage_service_id, + fetch_job_id, + ) + all_packages = all_packages + packages + + total_packages = package_lists_task.info["totalPackages"] + + total_aips = len([package for package in all_packages if package.is_aip()]) + total_sips = len([package for package in all_packages if package.is_sip()]) + total_dips = len([package for package in all_packages if package.is_dip()]) + total_deleted_aips = len( + [package for package in all_packages if package.is_deleted()] + ) + total_replicas = len([package for package in all_packages if package.is_replica()]) + + summary = "aips: '{}'; sips: '{}'; dips: '{}'; deleted: '{}'; replicated: '{}'".format( + total_aips, total_sips, total_dips, total_deleted_aips, total_replicas + ) + logger.info("%s", summary) + + obj = FetchJob.query.filter_by(id=fetch_job_id).first() + obj.total_packages = total_packages + obj.total_aips = total_aips + obj.total_dips = total_dips + obj.total_sips = total_sips + obj.total_replicas = total_replicas + obj.total_deleted_aips = total_deleted_aips + db.session.commit() + + +def _make_request(request_url, request_url_without_api_key): + """Make our request to the storage service and return a valid + response to our caller or raise a TaskError for celery. + """ + response = requests.get(request_url) + if response.status_code != requests.codes.ok: + err = "Check the URL and API details, cannot connect to: `{}`".format( + request_url_without_api_key + ) + logger.error(err) + raise TaskError("Bad response from server: {}".format(err)) + try: + packages = response.json() + except json.JSONDecodeError: + err = "Response is OK, but cannot decode JSON from server" + logger.error(err) + raise TaskError(err) + return packages + + +@celery.task(bind=True) +def package_lists_request(self, apiUrl, timestamp, packages_directory): + """Request package lists from the storage service. Package lists + will contain details of the AIPs that we want to download. + """ + META = "meta" + NEXT = "next" + LIMIT = "limit" + COUNT = "total_count" + IN_PROGRESS = "IN PROGRESS" + ( + base_url, + request_url_without_api_key, + request_url, + ) = format_api_url_with_limit_offset(apiUrl) + # First packages request. + packages = _make_request(request_url, request_url_without_api_key) + packages_count = 1 + # Calculate how many package list files will be downloaded based on + # total number of packages and the download limit + total_packages = int(packages.get(META, {}).get(COUNT, 0)) + total_package_lists = int(total_packages / int(apiUrl.get(LIMIT))) + ( + total_packages % int(apiUrl.get(LIMIT)) > 0 + ) + # There may be more packages to download to let's access those here. + # TODO: `request_url_without_api_key` at this point will not be as + # accurate. If we have more time, modify `format_api_url_with_limit_offset(...)` + # to work with raw offset and limit data to make up for the fact + # that an API key is plain-encoded in next_url. + next_url = packages.get(META, {}).get(NEXT, None) + write_packages_json(packages_count, packages, packages_directory) + while next_url is not None: + next_request = "{}{}".format(base_url, next_url) + next_packages = _make_request(next_request, request_url_without_api_key) + packages_count += 1 + write_packages_json(packages_count, next_packages, packages_directory) + next_url = next_packages.get(META, {}).get(NEXT, None) + self.update_state( + state=IN_PROGRESS, + meta={ + "message": "Total packages: {} Total package lists: {}".format( + total_packages, total_package_lists + ) + }, + ) + return { + "totalPackageLists": total_package_lists, + "totalPackages": total_packages, + "timestampStr": timestamp, + } + + +@celery.task() +def get_mets( + package_uuid, + relative_path_to_mets, + api_url, + timestamp_str, + package_list_no, + storage_service_id, + fetch_job_id, +): + """Request METS XML file from the storage service and parse. + + Download a METS file from an AIP that is stored in the storage + service and then parse the results into the AIPscan database. + + This function relies on being able to use mets-reader-writer which + is the primary object we will be passing about. + + TODO: Log METS errors. + """ + + download_file = _download_mets( + api_url, package_uuid, relative_path_to_mets, timestamp_str, package_list_no + ) + + try: + mets = parse_mets_with_metsrw(download_file) + except METSError: + # An error we need to log and report back to the user. + return + + try: + original_name = get_aip_original_name(mets) + except METSError: + # Some other error with the METS file that we might want to + # log and act upon. + original_name = package_uuid + + aip = database_helpers.create_aip_object( + package_uuid=package_uuid, + transfer_name=original_name, + create_date=mets.createdate, + storage_service_id=storage_service_id, + fetch_job_id=fetch_job_id, + ) + + database_helpers.process_aip_data(aip, mets) +
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_database_helpers.html b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_database_helpers.html new file mode 100644 index 00000000..94ba07d5 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_database_helpers.html @@ -0,0 +1,204 @@ + + + + + + + + AIPscan.Aggregator.tests.test_database_helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.tests.test_database_helpers

+# -*- coding: utf-8 -*-
+import os
+import uuid
+
+import metsrw
+import pytest
+
+from AIPscan.Aggregator import database_helpers
+from AIPscan.models import Agent, AIP
+
+FIXTURES_DIR = "fixtures"
+
+
+
[docs]def test_create_aip(app_instance): + """Test AIP creation.""" + PACKAGE_UUID = str(uuid.uuid4()) + TRANSFER_NAME = "some name" + STORAGE_SERVICE_ID = 1 + FETCH_JOB_ID = 1 + + database_helpers.create_aip_object( + package_uuid=PACKAGE_UUID, + transfer_name=TRANSFER_NAME, + create_date="2020-11-02", + storage_service_id=STORAGE_SERVICE_ID, + fetch_job_id=FETCH_JOB_ID, + ) + + aip = AIP.query.filter_by(uuid=PACKAGE_UUID).first() + assert aip is not None + assert aip.transfer_name == TRANSFER_NAME + assert aip.storage_service_id == STORAGE_SERVICE_ID + assert aip.fetch_job_id == FETCH_JOB_ID
+ + +
[docs]@pytest.mark.parametrize( + "fixture_path, event_count, agent_link_multiplier", + [ + (os.path.join("features_mets", "features-mets.xml"), 0, 0), + (os.path.join("iso_mets", "iso_mets.xml"), 17, 3), + (os.path.join("images_mets", "images.xml"), 76, 3), + ], +) +def test_event_creation( + app_instance, mocker, fixture_path, event_count, agent_link_multiplier +): + """Make sure that we're seeing all of the events associated with + an AIP and that they are potentially written to the database okay. + Make sure too that the event_agent_relationship is established. + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + mets_file = os.path.join(script_dir, FIXTURES_DIR, fixture_path) + mets = metsrw.METSDocument.fromfile(mets_file) + mocker.patch("AIPscan.models.Event") + agent_find_match = mocker.patch( + "AIPscan.Aggregator.database_helpers._create_agent_type_id" + ) + mocker.patch( + "sqlalchemy.orm.query.Query.first", + return_value=Agent( + linking_type_value="some_type_value", + agent_type="an_agent_type", + agent_value="an_agent_value", + ), + ) + mocked_events = mocker.patch("AIPscan.db.session.add") + mocker.patch("AIPscan.db.session.commit") + for fsentry in mets.all_files(): + database_helpers.create_event_objects(fsentry, "some_id") + assert mocked_events.call_count == event_count + assert agent_find_match.call_count == event_count * agent_link_multiplier
+ + +
[docs]@pytest.mark.parametrize( + "fixture_path, number_of_unique_agents", + [ + (os.path.join("features_mets", "features-mets.xml"), 0), + (os.path.join("features_mets", "features-mets-added-agents.xml"), 5), + (os.path.join("iso_mets", "iso_mets.xml"), 3), + (os.path.join("images_mets", "images.xml"), 3), + ], +) +def test_collect_agents(app_instance, fixture_path, number_of_unique_agents): + """Make sure that we retrieve only unique Agents from the METS to + then add to the database. Agents are "repeated" per PREMIS:OBJECT + in METS. + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + mets_file = os.path.join(script_dir, FIXTURES_DIR, fixture_path) + mets = metsrw.METSDocument.fromfile(mets_file) + agents = database_helpers.collect_mets_agents(mets) + assert len(agents) == number_of_unique_agents
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_mets.html b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_mets.html new file mode 100644 index 00000000..26ead61f --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_mets.html @@ -0,0 +1,168 @@ + + + + + + + + AIPscan.Aggregator.tests.test_mets — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.tests.test_mets

+# -*- coding: utf-8 -*-
+
+"""Tests for METS helper functions."""
+
+import os
+
+import metsrw
+import pytest
+
+from AIPscan.Aggregator import mets_parse_helpers
+
+FIXTURES_DIR = "fixtures"
+
+
+
[docs]@pytest.mark.parametrize( + "fixture_path, transfer_name, mets_error", + [ + (os.path.join("features_mets", "features-mets.xml"), "myTransfer", False), + (os.path.join("iso_mets", "iso_mets.xml"), "iso", False), + ( + os.path.join("original_name_mets", "document-empty-dirs.xml"), + "empty-dirs", + False, + ), + # Exception: Cannot disambiguate dmdSec_1 in the METS using + # mets-reader-writer and so we cannot retrieve the originalName. + (os.path.join("original_name_mets", "dataverse_example.xml"), "", True), + ], +) +def test_get_aip_original_name(fixture_path, transfer_name, mets_error): + """Make sure that we can reliably get original name from the METS + file given we haven't any mets-reader-writer helpers. + """ + script_dir = os.path.dirname(os.path.realpath(__file__)) + mets_file = os.path.join(script_dir, FIXTURES_DIR, fixture_path) + mets = metsrw.METSDocument.fromfile(mets_file) + if mets_error: + # Function should raise an error to work with. + with pytest.raises(mets_parse_helpers.METSError): + _ = mets_parse_helpers.get_aip_original_name(mets) + return + assert mets_parse_helpers.get_aip_original_name(mets) == transfer_name + # Test the same works with a string. + with open(mets_file, "rb") as mets_stream: + mets = metsrw.METSDocument.fromstring(mets_stream.read()) + assert mets_parse_helpers.get_aip_original_name(mets) == transfer_name + # Use that string to manipulate the text so that the element cannot + # be found. + with open(mets_file, "rb") as mets_stream: + # Remove originalName and test for an appropriate side-effect. + new_mets = mets_stream.read() + new_mets = new_mets.replace(b"originalName", b"originalNameIsNotHere") + mets = metsrw.METSDocument.fromstring(new_mets) + # Function should raise an error to work with. + with pytest.raises(mets_parse_helpers.METSError): + _ = mets_parse_helpers.get_aip_original_name(mets) == transfer_name
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_task_helpers.html b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_task_helpers.html new file mode 100644 index 00000000..2611484e --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_task_helpers.html @@ -0,0 +1,331 @@ + + + + + + + + AIPscan.Aggregator.tests.test_task_helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.tests.test_task_helpers

+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+import json
+import os
+
+import pytest
+
+from AIPscan.Aggregator.types import StorageServicePackage
+from AIPscan.Aggregator import task_helpers
+
+FIXTURES_DIR = "fixtures"
+
+
+
[docs]@pytest.mark.parametrize( + "input_date,output_date,now_year", + [ + ("2020-05-19T08:04:16+00:00", "2020-05-19T08:04:16", False), + ("2020-07-30T13:27:45.757482+00:00", "2020-07-30T13:27:45", False), + ("2020-07-30", "2020-07-30T00:00:00", False), + ("T13:27:45", "", True), + ("こんにちは世界", "1970-01-01T00:00:01", False), + ], +) +def test_tz_neutral_dates(input_date, output_date, now_year): + """Ensure datetime values are handled sensibly across regions. + """ + result_date = task_helpers._tz_neutral_date(input_date) + if now_year is True: + year = datetime.now().strftime("%Y-%m-%d") + output_date = "{}{}".format(year, input_date) + output_date = datetime.strptime(output_date, "%Y-%m-%dT%H:%M:%S") + assert result_date == output_date + else: + output_date = datetime.strptime(output_date, "%Y-%m-%dT%H:%M:%S") + assert result_date == output_date
+ + +
[docs]@pytest.mark.parametrize( + "url_api_dict, base_url, url_without_api_key, url_with_api_key", + [ + ( + { + "baseUrl": "http://example.com:9000/", + "limit": "23", + "offset": "13", + "userName": "test", + "apiKey": "mykey", + }, + "http://example.com:9000", + "http://example.com:9000/api/v2/file/?limit=23&offset=13", + "http://example.com:9000/api/v2/file/?limit=23&offset=13&username=test&api_key=mykey", + ), + ( + { + "baseUrl": "http://subdomain.example.com:8000/", + "limit": "10", + "offset": "99", + "userName": "anothertest", + "apiKey": "myotherkey", + }, + "http://subdomain.example.com:8000", + "http://subdomain.example.com:8000/api/v2/file/?limit=10&offset=99", + "http://subdomain.example.com:8000/api/v2/file/?limit=10&offset=99&username=anothertest&api_key=myotherkey", + ), + ], +) +def test_format_api_url(url_api_dict, base_url, url_without_api_key, url_with_api_key): + res1, res2, res3 = task_helpers.format_api_url_with_limit_offset(url_api_dict) + assert res1 == base_url + assert res2 == url_without_api_key + assert res3 == url_with_api_key
+ + +
[docs]@pytest.mark.parametrize( + "api_url, package_uuid, path_to_mets, result", + [ + ( + {"baseUrl": "http://example.com", "userName": "1234", "apiKey": "1234"}, + "1234", + "1234", + "http://example.com/api/v2/file/1234/extract_file/?relative_path_to_file=1234&username=1234&api_key=1234", + ), + ( + {"baseUrl": "http://example.com/", "userName": "1234", "apiKey": "1234"}, + "1234", + "1234", + "http://example.com/api/v2/file/1234/extract_file/?relative_path_to_file=1234&username=1234&api_key=1234", + ), + ], +) +def test_get_mets_url(api_url, package_uuid, path_to_mets, result): + """Ensure that the URL for retrieving METS is constructed properly. + """ + mets_url = task_helpers.get_mets_url(api_url, package_uuid, path_to_mets) + assert mets_url == result
+ + +
[docs]@pytest.mark.parametrize( + "timestamp, package_list_number, result", + [("1234", 1, "AIPscan/Aggregator/downloads/1234/mets/1")], +) +def test_create_numbered_subdirs(timestamp, package_list_number, result, mocker): + """Ensure that the logic that we use to create sub-directories for + storing METS is sound. + """ + + # Test that an unknown directory is created first time around and + # that the correct result is returned. + mocked_makedirs = mocker.patch("os.makedirs") + subdir_string = task_helpers.create_numbered_subdirs(timestamp, package_list_number) + assert mocked_makedirs.call_count == 1 + assert subdir_string == result + + # Test that if the path exists, we don't create the directory, and + # that the correct result is returned. + mocker.patch("os.path.exists", result=True) + mocked_makedirs = mocker.patch("os.makedirs") + subdir_string = task_helpers.create_numbered_subdirs(timestamp, package_list_number) + assert mocked_makedirs.call_count == 0 + assert subdir_string == result
+ + +
[docs]@pytest.fixture() +def packages(): + fixtures_path = os.path.join("package_json", "packages.json") + script_dir = os.path.dirname(os.path.realpath(__file__)) + packages_file = os.path.join(script_dir, FIXTURES_DIR, fixtures_path) + with open(packages_file, "r") as package_json: + packages = json.load(package_json) + return packages.get("objects")
+ + +
[docs]@pytest.mark.parametrize( + "idx, storage_service_package", + [ + ( + 0, + StorageServicePackage( + aip=True, + current_path="9194/0daf/5c33/4670/bfc8/9108/f32b/ca7b/repl-91940daf-5c33-4670-bfc8-9108f32bca7b.7z", + uuid="91940daf-5c33-4670-bfc8-9108f32bca7b", + ), + ), + ( + 1, + StorageServicePackage( + replica=True, + aip=True, + current_path="9021/92d9/6232/4d5f/b6c4/dfa7/b873/3960/repl-902192d9-6232-4d5f-b6c4-dfa7b8733960.7z", + uuid="902192d9-6232-4d5f-b6c4-dfa7b8733960", + ), + ), + ( + 2, + StorageServicePackage( + deleted=True, + aip=True, + current_path="594a/03f3/d8eb/4c83/affd/aefa/f75d/53cc/delete_me-594a03f3-d8eb-4c83-affd-aefaf75d53cc.7z", + uuid="594a03f3-d8eb-4c83-affd-aefaf75d53cc", + ), + ), + ( + 3, + StorageServicePackage( + aip=True, + current_path="e422/c724/834c/4164/a7be/4c88/1043/a531/normalize-e422c724-834c-4164-a7be-4c881043a531.7z", + uuid="e422c724-834c-4164-a7be-4c881043a531", + ), + ), + ( + 4, + StorageServicePackage( + aip=True, + current_path="583c/009c/4255/44b9/8868/f57e/4bad/e93c/normal_aip-583c009c-4255-44b9-8868-f57e4bade93c.7z", + uuid="583c009c-4255-44b9-8868-f57e4bade93c", + ), + ), + ( + 5, + StorageServicePackage( + aip=True, + current_path="846f/ca2b/0919/4673/804c/0f62/6a30/cabd/uncomp-846fca2b-0919-4673-804c-0f626a30cabd", + uuid="846fca2b-0919-4673-804c-0f626a30cabd", + ), + ), + ( + 6, + StorageServicePackage( + dip=True, + current_path="1555/04f5/98dd/48d1/9e97/83bc/f0a5/efa2/normcore-59f70134-eeca-4886-888e-b2013a08571e", + uuid="155504f5-98dd-48d1-9e97-83bcf0a5efa2", + ), + ), + ( + 7, + StorageServicePackage( + aip=True, + current_path="59f7/0134/eeca/4886/888e/b201/3a08/571e/normcore-59f70134-eeca-4886-888e-b2013a08571e", + uuid="59f70134-eeca-4886-888e-b2013a08571e", + ), + ), + ( + 8, + StorageServicePackage( + sip=True, + current_path="originals/backlog-fbdcd607-270e-4dff-9a01-d11b7c2a0200", + uuid="fbdcd607-270e-4dff-9a01-d11b7c2a0200", + ), + ), + ], +) +def test_process_package_object(packages, idx, storage_service_package): + """Test our ability to collect information from a package list + object and parse it into a storage service package type for + further use. + """ + package_obj = task_helpers.process_package_object(packages[idx]) + assert package_obj == storage_service_package, idx
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_types.html b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_types.html new file mode 100644 index 00000000..70b6072c --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/tests/test_types.html @@ -0,0 +1,268 @@ + + + + + + + + AIPscan.Aggregator.tests.test_types — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.tests.test_types

+# -*- coding: utf-8 -*-
+
+import pytest
+
+from AIPscan.Aggregator import types
+
+
+
[docs]def test_storage_service_package_init(): + """Test that the type constructor works as expected.""" + package_1 = types.StorageServicePackage() + package_2 = types.StorageServicePackage() + assert package_1 == package_2 + assert package_1.aip is False + assert package_1.dip is False + assert package_1.sip is False + assert package_1.replica is False + assert package_1.deleted is False
+ + +
[docs]def test_storage_service_package_eq(): + """Provide some other equality tests for the type.""" + package_1 = types.StorageServicePackage( + deleted=True, replica=False, uuid="1", current_path="2" + ) + package_2 = types.StorageServicePackage( + deleted=True, replica=False, uuid="1", current_path="2" + ) + assert package_1 == package_2 + assert package_1.replica is False + assert package_1.deleted is True + package_3 = types.StorageServicePackage( + deleted=1, replica=2, aip=3, dip=4, sip=5, uuid="6", current_path="7" + ) + package_4 = types.StorageServicePackage( + deleted=1, replica=2, aip=3, dip=4, sip=5, uuid="6", current_path="7" + ) + assert package_3 == package_4 + assert package_3 != package_1
+ + +
[docs]def test_package_ness(): + """Given specific package types check that they evaluate to true + when queried. + """ + package = types.StorageServicePackage(aip=True) + assert package.is_aip() + assert not package.is_dip() + assert not package.is_sip() + assert not package.is_deleted() + assert not package.is_replica() + package = types.StorageServicePackage(dip=True) + assert package.is_dip() + assert not package.is_aip() + assert not package.is_sip() + assert not package.is_deleted() + assert not package.is_replica() + package = types.StorageServicePackage(sip=True) + assert package.is_sip() + assert not package.is_dip() + assert not package.is_aip() + assert not package.is_deleted() + assert not package.is_replica() + package = types.StorageServicePackage(deleted=True) + assert package.is_deleted() + assert not package.is_replica() + assert not package.is_aip() + assert not package.is_dip() + assert not package.is_aip() + package = types.StorageServicePackage(aip=True, replica=True) + assert package.is_replica() + assert not package.is_deleted() + assert not package.is_aip() + assert not package.is_dip() + assert not package.is_aip()
+ + +
[docs]@pytest.mark.parametrize( + "package, result", + [ + ( + types.StorageServicePackage( + uuid="91940daf-5c33-4670-bfc8-9108f32bca7b", + current_path="9194/0daf/5c33/4670/bfc8/9108/f32b/ca7b/repl-91940daf-5c33-4670-bfc8-9108f32bca7b.7z", + ), + "repl-91940daf-5c33-4670-bfc8-9108f32bca7b/data/METS.91940daf-5c33-4670-bfc8-9108f32bca7b.xml", + ), + ( + types.StorageServicePackage( + uuid="902192d9-6232-4d5f-b6c4-dfa7b8733960", + current_path="9021/92d9/6232/4d5f/b6c4/dfa7/b873/3960/repl-902192d9-6232-4d5f-b6c4-dfa7b8733960.7z", + ), + "repl-902192d9-6232-4d5f-b6c4-dfa7b8733960/data/METS.902192d9-6232-4d5f-b6c4-dfa7b8733960.xml", + ), + ( + types.StorageServicePackage( + deleted=True, + uuid="594a03f3-d8eb-4c83-affd-aefaf75d53cc", + current_path="594a/03f3/d8eb/4c83/affd/aefa/f75d/53cc/delete_me-594a03f3-d8eb-4c83-affd-aefaf75d53cc.7z", + ), + None, + ), + ( + types.StorageServicePackage( + uuid="e422c724-834c-4164-a7be-4c881043a531", + current_path="e422/c724/834c/4164/a7be/4c88/1043/a531/normalize-e422c724-834c-4164-a7be-4c881043a531.7z", + ), + "normalize-e422c724-834c-4164-a7be-4c881043a531/data/METS.e422c724-834c-4164-a7be-4c881043a531.xml", + ), + ( + types.StorageServicePackage( + uuid="583c009c-4255-44b9-8868-f57e4bade93c", + current_path="583c/009c/4255/44b9/8868/f57e/4bad/e93c/normal_aip-583c009c-4255-44b9-8868-f57e4bade93c.7z", + ), + "normal_aip-583c009c-4255-44b9-8868-f57e4bade93c/data/METS.583c009c-4255-44b9-8868-f57e4bade93c.xml", + ), + ( + types.StorageServicePackage( + uuid="846fca2b-0919-4673-804c-0f626a30cabd", + current_path="846f/ca2b/0919/4673/804c/0f62/6a30/cabd/uncomp-846fca2b-0919-4673-804c-0f626a30cabd", + ), + "uncomp-846fca2b-0919-4673-804c-0f626a30cabd/data/METS.846fca2b-0919-4673-804c-0f626a30cabd.xml", + ), + ( + types.StorageServicePackage( + dip=True, + uuid="59f70134-eeca-4886-888e-b2013a08571e", + current_path="1555/04f5/98dd/48d1/9e97/83bc/f0a5/efa2/normcore-59f70134-eeca-4886-888e-b2013a08571e", + ), + None, + ), + ( + types.StorageServicePackage( + uuid="59f70134-eeca-4886-888e-b2013a08571e", + current_path="59f7/0134/eeca/4886/888e/b201/3a08/571e/normcore-59f70134-eeca-4886-888e-b2013a08571e", + ), + "normcore-59f70134-eeca-4886-888e-b2013a08571e/data/METS.59f70134-eeca-4886-888e-b2013a08571e.xml", + ), + ( + types.StorageServicePackage( + sip=True, + uuid="fbdcd607-270e-4dff-9a01-d11b7c2a0200", + current_path="originals/backlog-fbdcd607-270e-4dff-9a01-d11b7c2a0200", + ), + None, + ), + ], +) +def test_get_relative_path(package, result): + """Given the current_path check our ability to construct a relative + path within a METS file. + """ + if package.replica or package.sip or package.deleted or package.dip: + with pytest.raises(types.PackageError): + _ = package.get_relative_path() + else: + assert package.get_relative_path() == result
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/types.html b/docs/_build/html/_modules/AIPscan/Aggregator/types.html new file mode 100644 index 00000000..e2055c66 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/types.html @@ -0,0 +1,258 @@ + + + + + + + + AIPscan.Aggregator.types — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.types

+# -*- coding: utf-8 -*-
+
+
+
[docs]class PackageError(Exception): + """There are things we cannot do with the package type unless it + is completed properly. Let the user know. + """
+ + +
[docs]class StorageServicePackage(object): + """Type that can record information about a storage service package + and provide helpers as to whether or not we should process it. + """ + + default_pair_tree = "0000-0000-0000-0000-0000-0000-0000-0000-" + compressed_ext = ".7z" + + def __init__(self, **kwargs): + """Package constructor""" + + DELETED = "deleted" + REPLICA = "replica" + AIP = "aip" + DIP = "dip" + SIP = "sip" + + UUID = "uuid" + CURRENT_PATH = "current_path" + + self.deleted = False + self.replica = False + self.aip = False + self.dip = False + self.sip = False + self.uuid = None + self.current_path = None + + if kwargs: + self.deleted = kwargs.get(DELETED, self.deleted) + self.replica = kwargs.get(REPLICA, self.replica) + self.aip = kwargs.get(AIP, self.aip) + self.dip = kwargs.get(DIP, self.dip) + self.sip = kwargs.get(SIP, self.sip) + self.uuid = kwargs.get(UUID, self.uuid) + self.current_path = kwargs.get(CURRENT_PATH, self.uuid) + + def __repr__(self): + ret = "aip: '{}'; dip: '{}'; sip: '{}'; deleted: '{}'; replica: '{}';".format( + self.aip, self.dip, self.sip, self.deleted, self.replica + ) + return ret + + def __eq__(self, other): + """Comparison operator""" + ret = True + if self.aip != other.aip: + ret = False + if self.dip != other.dip: + ret = False + if self.sip != other.sip: + ret = False + if self.deleted != other.deleted: + ret = False + if self.replica != other.replica: + ret = False + if self.uuid != other.uuid: + ret = False + if self.current_path != other.current_path: + ret = False + return ret + +
[docs] def is_aip(self): + """Determine whether the package is a AIP""" + if ( + self.aip + and not self.deleted + and not self.replica + and not self.dip + and not self.sip + ): + return True + return False
+ +
[docs] def is_dip(self): + """Determine whether the package is a DIP""" + if ( + self.dip + and not self.deleted + and not self.replica + and not self.aip + and not self.sip + ): + return True + return False
+ +
[docs] def is_sip(self): + """Determine whether the package is a SIP""" + if ( + self.sip + and not self.deleted + and not self.replica + and not self.dip + and not self.aip + ): + return True + return False
+ +
[docs] def is_replica(self): + """Determine whether the package is a replica package""" + if ( + self.replica + and self.aip + and not self.deleted + and not self.sip + and not self.dip + ): + return True + return False
+ +
[docs] def is_deleted(self): + """Determine whether the package is a deleted package""" + if self.deleted: + return True + return False
+ +
[docs] def get_relative_path(self): + """Return relative path from current_path.""" + if self.dip or self.sip or self.replica: + raise PackageError( + "Get relative path for sip or replica packages not yet implemented" + ) + if self.deleted: + raise PackageError("There are no relative paths for deleted packages") + if self.uuid is None: + raise PackageError("Cannot generate a relative path without a package UUID") + rel = "" + left_offset = len(self.default_pair_tree) + right_offset = -len(self.compressed_ext) + try: + if self.current_path.endswith(self.compressed_ext): + rel = self.current_path[left_offset:right_offset] + else: + rel = self.current_path[left_offset:] + except AttributeError: + raise PackageError("Current path doesn't exist for the package") + return "{}/data/METS.{}.xml".format(rel, self.uuid)
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Aggregator/views.html b/docs/_build/html/_modules/AIPscan/Aggregator/views.html new file mode 100644 index 00000000..53696051 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Aggregator/views.html @@ -0,0 +1,408 @@ + + + + + + + + AIPscan.Aggregator.views — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Aggregator.views

+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+import os
+import shutil
+
+from celery.result import AsyncResult
+from flask import Blueprint, render_template, redirect, request, flash, url_for, jsonify
+
+from AIPscan import db
+from AIPscan.Aggregator.task_helpers import get_packages_directory
+from AIPscan.Aggregator.forms import StorageServiceForm
+from AIPscan.Aggregator import tasks
+from AIPscan.extensions import celery
+from AIPscan.models import (
+    FetchJob,
+    StorageService,
+    # Custom celery Models.
+    package_tasks,
+    get_mets_tasks,
+)
+
+aggregator = Blueprint("aggregator", __name__, template_folder="templates")
+
+
+# PICTURAE TODO: Starting to see patterns shared across modules, e.g.
+# date handling in the data module and in here. Let's bring those
+# together in a helpful kind of way.
+def _split_ms(date_string):
+    """Remove microseconds from the given date string."""
+    return str(date_string).split(".")[0]
+
+
+def _format_date(date_string):
+    """Format date to something nicer that can played back in reports"""
+    DATE_FORMAT_FULL = "%Y-%m-%d %H:%M:%S"
+    DATE_FORMAT_PARTIAL = "%Y-%m-%d"
+    formatted_date = datetime.strptime(_split_ms(date_string), DATE_FORMAT_FULL)
+    return formatted_date.strftime(DATE_FORMAT_PARTIAL)
+
+
+
[docs]@aggregator.route("/", methods=["GET"]) +def ss_default(): + # load the default storage service + storage_service = StorageService.query.filter_by(default=True).first() + if storage_service is None: + # no default is set, retrieve the first storage service + storage_service = StorageService.query.first() + # there are no storage services defined at all + if storage_service is None: + return redirect(url_for("aggregator.ss")) + mets_fetch_jobs = FetchJob.query.filter_by( + storage_service_id=storage_service.id + ).all() + return render_template( + "storage_service.html", + storage_service=storage_service, + mets_fetch_jobs=mets_fetch_jobs, + )
+ + +
[docs]@aggregator.route("/storage_service/<id>", methods=["GET"]) +def storage_service(id): + storage_service = StorageService.query.get(id) + mets_fetch_jobs = FetchJob.query.filter_by(storage_service_id=id).all() + return render_template( + "storage_service.html", + storage_service=storage_service, + mets_fetch_jobs=mets_fetch_jobs, + )
+ + +
[docs]@aggregator.route("/storage_services", methods=["GET"]) +def ss(): + storage_services = StorageService.query.all() + return render_template("storage_services.html", storage_services=storage_services)
+ + +
[docs]@aggregator.route("/edit_storage_service/<id>", methods=["GET", "POST"]) +def edit_storage_service(id): + form = StorageServiceForm() + storage_service = StorageService.query.get(id) + if request.method == "GET": + form.name.data = storage_service.name + form.url.data = storage_service.url + form.user_name.data = storage_service.user_name + form.download_limit.data = storage_service.download_limit + form.download_offset.data = storage_service.download_offset + form.api_key.data = storage_service.api_key + form.default.data = storage_service.default + if form.validate_on_submit(): + storage_service.name = form.name.data + storage_service.url = form.url.data + storage_service.user_name = form.user_name.data + storage_service.api_key = form.api_key.data + storage_service.download_limit = form.download_limit.data + storage_service.download_offset = form.download_offset.data + if form.default.data is True: + storage_services = StorageService.query.all() + for ss in storage_services: + ss.default = False + storage_service.default = form.default.data + db.session.commit() + flash("Storage service {} updated".format(form.name.data)) + return redirect(url_for("aggregator.ss")) + return render_template( + "edit_storage_service.html", title="Storage Service", form=form + )
+ + +
[docs]@aggregator.route("/new_storage_service", methods=["GET", "POST"]) +def new_storage_service(): + form = StorageServiceForm() + if form.validate_on_submit(): + ss = StorageService( + name=form.name.data, + url=form.url.data, + user_name=form.user_name.data, + api_key=form.api_key.data, + download_limit=form.download_limit.data, + download_offset=form.download_offset.data, + default=form.default.data, + ) + db.session.add(ss) + db.session.commit() + flash("New storage service {} created".format(form.name.data)) + return redirect(url_for("aggregator.ss")) + return render_template( + "edit_storage_service.html", title="Storage Service", form=form + )
+ + +
[docs]@aggregator.route("/delete_storage_service/<id>", methods=["GET"]) +def delete_storage_service(id): + storage_service = StorageService.query.get(id) + mets_fetch_jobs = FetchJob.query.filter_by(storage_service_id=id).all() + for mets_fetch_job in mets_fetch_jobs: + if os.path.exists(mets_fetch_job.download_directory): + shutil.rmtree(mets_fetch_job.download_directory) + db.session.delete(storage_service) + db.session.commit() + flash("Storage service '{}' is deleted".format(storage_service.name)) + return redirect(url_for("aggregator.ss"))
+ + +
[docs]@aggregator.route("/new_fetch_job/<id>", methods=["POST"]) +def new_fetch_job(id): + + # this function is triggered by the Javascript attached to the "New Fetch Job" button + + storage_service = StorageService.query.get(id) + api_url = { + "baseUrl": storage_service.url, + "userName": storage_service.user_name, + "apiKey": storage_service.api_key, + "offset": str(storage_service.download_offset), + "limit": str(storage_service.download_limit), + } + + # create "downloads/" directory if it doesn't exist + if not os.path.exists("AIPscan/Aggregator/downloads/"): + os.makedirs("AIPscan/Aggregator/downloads/") + + # create a subdirectory for the download job using a timestamp as its name + datetime_obj_start = datetime.now().replace(microsecond=0) + timestamp_str = datetime_obj_start.strftime("%Y-%m-%d-%H-%M-%S") + timestamp = datetime_obj_start.strftime("%Y-%m-%d %H:%M:%S") + os.makedirs("AIPscan/Aggregator/downloads/" + timestamp_str + "/packages/") + os.makedirs("AIPscan/Aggregator/downloads/" + timestamp_str + "/mets/") + + # create a fetch_job record in the aipscan database + # write fetch job info to database + fetch_job = FetchJob( + total_packages=None, + total_deleted_aips=None, + total_aips=None, + download_start=datetime_obj_start, + download_end=None, + download_directory="AIPscan/Aggregator/downloads/" + timestamp_str + "/", + storage_service_id=storage_service.id, + ) + db.session.add(fetch_job) + db.session.commit() + + packages_directory = get_packages_directory(timestamp_str) + + # send the METS fetch job to a background job that will coordinate other workers + task = tasks.workflow_coordinator.delay( + api_url, timestamp_str, storage_service.id, fetch_job.id, packages_directory + ) + + """ + # this only works on the first try, after that Flask is not able to get task info from Celery + # the compromise is to write the task ID from the Celery worker to its SQLite backend + + coordinator_task = tasks.workflow_coordinator.AsyncResult(task.id, app=celery) + taskId = coordinator_task.info.get("package_lists_taskId") + response = {"timestamp": timestamp, "taskId": taskId} + """ + + # Run a while loop in case the workflow coordinator task hasn't + # finished writing to database yet + while True: + obj = package_tasks.query.filter_by(workflow_coordinator_id=task.id).first() + try: + task_id = obj.package_task_id + if task_id is not None: + break + except AttributeError: + continue + + # Send response back to JavaScript function that was triggered by + # the 'New Fetch Job' button. + response = {"timestamp": timestamp, "taskId": task_id, "fetchJobId": fetch_job.id} + return jsonify(response)
+ + +
[docs]@aggregator.route("/delete_fetch_job/<id>", methods=["GET"]) +def delete_fetch_job(id): + fetch_job = FetchJob.query.get(id) + storage_service = StorageService.query.get(fetch_job.storage_service_id) + if os.path.exists(fetch_job.download_directory): + shutil.rmtree(fetch_job.download_directory) + db.session.delete(fetch_job) + db.session.commit() + flash("Fetch job {} is deleted".format(fetch_job.download_start)) + return redirect(url_for("aggregator.storage_service", id=storage_service.id))
+ + +
[docs]@aggregator.route("/package_list_task_status/<taskid>") +def task_status(taskid): + task = tasks.package_lists_request.AsyncResult(taskid, app=celery) + if task.state == "PENDING": + # job did not start yet + response = {"state": task.state} + elif task.state != "FAILURE": + if task.state == "SUCCESS": + obj = package_tasks.query.filter_by(package_task_id=task.id).first() + coordinator_id = obj.workflow_coordinator_id + response = {"state": task.state, "coordinatorId": coordinator_id} + else: + response = {"state": task.state, "message": task.info.get("message")} + # CR Note: I am not sure we will ever meet this else, because + # task.state will always be intercepted through != FAILUTE. IDK. Do + # you read it the same way? + else: + # something went wrong in the background job + response = { + "state": task.state, + "status": str(task.info), # this is the exception raised + } + return jsonify(response)
+ + +
[docs]@aggregator.route("/get_mets_task_status/<coordinatorid>") +def get_mets_task_status(coordinatorid): + """Get mets task status""" + totalAIPs = int(request.args.get("totalAIPs")) + fetchJobId = int(request.args.get("fetchJobId")) + mets_tasks = get_mets_tasks.query.filter_by( + workflow_coordinator_id=coordinatorid, status=None + ).all() + response = [] + for row in mets_tasks: + task_id = row.get_mets_task_id + package_uuid = row.package_uuid + task_result = AsyncResult(id=task_id, app=celery) + mets_task_status = task_result.state + if mets_task_status is None: + continue + if (mets_task_status == "SUCCESS") or (mets_task_status[0] == "FAILURE"): + totalAIPs += 1 + response.append( + { + "state": mets_task_status, + "package": "METS.{}.xml".format(package_uuid), + "totalAIPs": totalAIPs, + } + ) + obj = get_mets_tasks.query.filter_by(get_mets_task_id=task_id).first() + obj.status = mets_task_status + db.session.commit() + if len(mets_tasks) != 0: + return jsonify(response) + if totalAIPs == 0: + response = {"state": "PENDING"} + return jsonify(response) + downloadEnd = datetime.now().replace(microsecond=0) + obj = FetchJob.query.filter_by(id=fetchJobId).first() + start = obj.download_start + downloadStart = _format_date(start) + obj.download_end = downloadEnd + db.session.commit() + response = {"state": "COMPLETED"} + flash("Fetch Job {} completed".format(downloadStart)) + return jsonify(response)
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Data/data.html b/docs/_build/html/_modules/AIPscan/Data/data.html new file mode 100644 index 00000000..d0c472de --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Data/data.html @@ -0,0 +1,406 @@ + + + + + + + + AIPscan.Data.data — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Data.data

+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+
+from AIPscan.models import AIP, File, FileType, StorageService
+
+
+FIELD_AIP = "AIP"
+FIELD_AIP_ID = "AIPID"
+FIELD_AIP_NAME = "AIPName"
+FIELD_AIP_SIZE = "AIPSize"
+FIELD_AIP_UUID = "AIPUUID"
+FIELD_AIPS = "AIPs"
+FIELD_ALL_AIPS = "AllAIPs"
+
+FIELD_COUNT = "Count"
+FIELD_CREATED_DATE = "CreatedDate"
+
+FIELD_DERIVATIVE_COUNT = "DerivativeCount"
+FIELD_DERIVATIVE_FORMAT = "DerivativeFormat"
+FIELD_DERIVATIVE_UUID = "DerivativeUUID"
+
+FIELD_FILES = "Files"
+FIELD_FILE_COUNT = "FileCount"
+FIELD_FILE_TYPE = "FileType"
+FIELD_FILENAME = "Filename"
+FIELD_FORMAT = "Format"
+FIELD_FORMATS = "Formats"
+
+FIELD_NAME = "Name"
+
+FIELD_ORIGINAL_UUID = "OriginalUUID"
+FIELD_ORIGINAL_FORMAT = "OriginalFormat"
+
+FIELD_PUID = "PUID"
+
+FIELD_RELATED_PAIRING = "RelatedPairing"
+
+FIELD_SIZE = "Size"
+FIELD_STORAGE_NAME = "StorageName"
+
+FIELD_TRANSFER_NAME = "TransferName"
+
+FIELD_UUID = "UUID"
+
+FIELD_VERSION = "Version"
+
+
+def _get_storage_service(storage_service_id):
+    DEFAULT_STORAGE_SERVICE_ID = 1
+    if storage_service_id == 0 or storage_service_id is None:
+        storage_service_id = DEFAULT_STORAGE_SERVICE_ID
+    storage_service = StorageService.query.get(storage_service_id)
+    return StorageService.query.first() if not storage_service else storage_service
+
+
+def _split_ms(date_string):
+    """Remove microseconds from the given date string."""
+    return str(date_string).split(".")[0]
+
+
+def _format_date(date_string):
+    """Format date to something nicer that can played back in reports"""
+    DATE_FORMAT_FULL = "%Y-%m-%d %H:%M:%S"
+    DATE_FORMAT_PARTIAL = "%Y-%m-%d"
+    formatted_date = datetime.strptime(_split_ms(date_string), DATE_FORMAT_FULL)
+    return formatted_date.strftime(DATE_FORMAT_PARTIAL)
+
+
+
[docs]def aip_overview(storage_service_id, original_files=True): + """Return a summary overview of all AIPs in a given storage service + """ + report = {} + storage_service = _get_storage_service(storage_service_id) + aips = AIP.query.filter_by(storage_service_id=storage_service.id).all() + for aip in aips: + files = None + if original_files is True: + files = File.query.filter_by(aip_id=aip.id, file_type=FileType.original) + else: + files = File.query.filter_by(aip_id=aip.id, file_type=FileType.preservation) + for file_ in files: + # Originals have PUIDs but Preservation Masters don't. + # Return a key (PUID or Format Name) for our report based on that. + try: + format_key = file_.puid + except AttributeError: + format_key = file_.file_format + if format_key in report: + report[format_key][FIELD_COUNT] = report[format_key][FIELD_COUNT] + 1 + if aip.uuid not in report[format_key][FIELD_AIPS]: + report[format_key][FIELD_AIPS].append(aip.uuid) + else: + report[format_key] = {} + report[format_key][FIELD_COUNT] = 1 + try: + report[format_key][FIELD_VERSION] = file_.format_version + report[format_key][FIELD_NAME] = file_.file_format + except AttributeError: + pass + if report[format_key].get(FIELD_AIPS) is None: + report[format_key][FIELD_AIPS] = [] + report[format_key][FIELD_AIPS].append(aip.uuid) + return report
+ + +
[docs]def aip_overview_two(storage_service_id, original_files=True): + """Return a summary overview of all AIPs in a given storage service + """ + report = {} + formats = {} + storage_service = _get_storage_service(storage_service_id) + aips = AIP.query.filter_by(storage_service_id=storage_service.id).all() + for aip in aips: + report[aip.uuid] = {} + report[aip.uuid][FIELD_AIP_NAME] = aip.transfer_name + report[aip.uuid][FIELD_CREATED_DATE] = _format_date(aip.create_date) + report[aip.uuid][FIELD_AIP_SIZE] = 0 + report[aip.uuid][FIELD_FORMATS] = {} + files = None + format_key = None + if original_files is True: + files = File.query.filter_by(aip_id=aip.id, file_type=FileType.original) + else: + files = File.query.filter_by(aip_id=aip.id, file_type=FileType.preservation) + for file_ in files: + try: + format_key = file_.puid + except AttributeError: + format_key = file_.file_format + if format_key is None: + continue + try: + formats[format_key] = "{} {}".format( + file_.file_format, file_.format_version + ) + except AttributeError: + formats[format_key] = "{}".format(file_.file_format) + size = report[aip.uuid][FIELD_AIP_SIZE] + try: + report[aip.uuid][FIELD_AIP_SIZE] = size + file_.size + # TODO: Find out why size is sometimes None. + except TypeError: + report[aip.uuid][FIELD_AIP_SIZE] = size + pass + if format_key not in report[aip.uuid][FIELD_FORMATS]: + report[aip.uuid][FIELD_FORMATS][format_key] = {} + report[aip.uuid][FIELD_FORMATS][format_key][FIELD_COUNT] = 1 + try: + report[aip.uuid][FIELD_FORMATS][format_key][ + FIELD_VERSION + ] = file_.format_version + report[aip.uuid][FIELD_FORMATS][format_key][ + FIELD_NAME + ] = file_.file_format + except AttributeError: + pass + else: + count = report[aip.uuid][FIELD_FORMATS][format_key][FIELD_COUNT] + report[aip.uuid][FIELD_FORMATS][format_key][FIELD_COUNT] = count + 1 + + report[FIELD_FORMATS] = formats + report[FIELD_STORAGE_NAME] = storage_service.name + return report
+ + +
[docs]def derivative_overview(storage_service_id): + """Return a summary of derivatives across AIPs with a mapping + created between the original format and the preservation copy. + """ + report = {} + storage_service = _get_storage_service(storage_service_id) + aips = AIP.query.filter_by(storage_service_id=storage_service.id).all() + all_aips = [] + for aip in aips: + if not aip.preservation_file_count > 0: + continue + + aip_report = {} + aip_report[FIELD_TRANSFER_NAME] = aip.transfer_name + aip_report[FIELD_UUID] = aip.uuid + aip_report[FIELD_FILE_COUNT] = aip.original_file_count + aip_report[FIELD_DERIVATIVE_COUNT] = aip.preservation_file_count + aip_report[FIELD_RELATED_PAIRING] = [] + + original_files = File.query.filter_by( + aip_id=aip.id, file_type=FileType.original + ) + for original_file in original_files: + preservation_derivative = File.query.filter_by( + file_type=FileType.preservation, original_file_id=original_file.id + ).first() + + if preservation_derivative is None: + continue + + file_derivative_pair = {} + file_derivative_pair[FIELD_DERIVATIVE_UUID] = preservation_derivative.uuid + file_derivative_pair[FIELD_ORIGINAL_UUID] = original_file.uuid + original_format_version = original_file.format_version + if original_format_version is None: + original_format_version = "" + file_derivative_pair[FIELD_ORIGINAL_FORMAT] = "{} {} ({})".format( + original_file.file_format, original_format_version, original_file.puid + ) + file_derivative_pair[FIELD_DERIVATIVE_FORMAT] = "{}".format( + preservation_derivative.file_format + ) + aip_report[FIELD_RELATED_PAIRING].append(file_derivative_pair) + + all_aips.append(aip_report) + + report[FIELD_ALL_AIPS] = all_aips + report[FIELD_STORAGE_NAME] = storage_service.name + + return report
+ + +def _largest_files_query(storage_service_id, file_type, limit): + """Fetch file information from database for largest files query + + This is separated into its own helper function to aid in testing. + """ + VALID_FILE_TYPES = set(item.value for item in FileType) + if file_type is not None and file_type in VALID_FILE_TYPES: + files = ( + File.query.join(AIP) + .join(StorageService) + .filter(StorageService.id == storage_service_id) + .filter(File.file_type == file_type) + .order_by(File.size.desc()) + .limit(limit) + ) + else: + files = ( + File.query.join(AIP) + .join(StorageService) + .filter(StorageService.id == storage_service_id) + .order_by(File.size.desc()) + .limit(limit) + ) + return files + + +
[docs]def largest_files(storage_service_id, file_type=None, limit=20): + """Return a summary of the largest files in a given Storage Service + + :param storage_service_id: Storage Service ID. + :param file_type: Optional filter for type of file to return + (acceptable values are "original" or "preservation"). + :param limit: Upper limit of number of results to return. + + :returns: "report" dict containing following fields: + report["StorageName"]: Name of Storage Service queried + report["Files"]: List of result files ordered desc by size + """ + report = {} + report[FIELD_FILES] = [] + storage_service = _get_storage_service(storage_service_id) + report[FIELD_STORAGE_NAME] = storage_service.name + + files = _largest_files_query(storage_service_id, file_type, limit) + + for file_ in files: + file_info = {} + + file_info["id"] = file_.id + file_info[FIELD_UUID] = file_.uuid + file_info[FIELD_NAME] = file_.name + file_info[FIELD_SIZE] = int(file_.size) + file_info[FIELD_AIP_ID] = file_.aip_id + file_info[FIELD_FILE_TYPE] = file_.file_type.value + + try: + file_info[FIELD_FORMAT] = file_.file_format + except AttributeError: + pass + try: + file_info[FIELD_VERSION] = file_.format_version + except AttributeError: + pass + try: + file_info[FIELD_PUID] = file_.puid + except AttributeError: + pass + + matching_aip = AIP.query.get(file_.aip_id) + if matching_aip is not None: + file_info[FIELD_AIP_NAME] = matching_aip.transfer_name + file_info[FIELD_AIP_UUID] = matching_aip.uuid + + report[FIELD_FILES].append(file_info) + + return report
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Data/tests/test_largest_files.html b/docs/_build/html/_modules/AIPscan/Data/tests/test_largest_files.html new file mode 100644 index 00000000..d63485b0 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Data/tests/test_largest_files.html @@ -0,0 +1,250 @@ + + + + + + + + AIPscan.Data.tests.test_largest_files — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Data.tests.test_largest_files

+# -*- coding: utf-8 -*-
+
+import datetime
+import pytest
+import uuid
+
+from AIPscan.Data import data
+from AIPscan.models import AIP, File, FileType, StorageService
+
+TEST_FILES = [
+    File(
+        uuid=uuid.uuid4(),
+        name="test.csv",
+        size=1234567,
+        aip_id=1,
+        file_type=FileType.original,
+        file_format="Comma Separated Values",
+        filepath="/path/to/file.csv",
+        date_created=datetime.datetime.now(),
+        checksum_type="md5",
+        checksum_value="fakemd5",
+    ),
+    File(
+        uuid=uuid.uuid4(),
+        name="test.txt",
+        size=12345,
+        aip_id=2,
+        file_type=FileType.original,
+        file_format="Plain Text File",
+        puid="x-fmt/111",
+        filepath="/path/to/file.txt",
+        date_created=datetime.datetime.now(),
+        checksum_type="md5",
+        checksum_value="anotherfakemd5",
+    ),
+    File(
+        uuid=uuid.uuid4(),
+        name="test.pdf",
+        size=12345678,
+        aip_id=1,
+        file_type=FileType.preservation,
+        file_format="Acrobat PDF/A - Portable Document Format",
+        format_version="1b",
+        filepath="/path/to/test.pdf",
+        date_created=datetime.datetime.now(),
+        checksum_type="md5",
+        checksum_value="yetanotherfakemd5",
+        original_file_id=1,
+    ),
+]
+
+MOCK_STORAGE_SERVICE_ID = 1
+MOCK_STORAGE_SERVICE_NAME = "some name"
+TEST_STORAGE_SERVICE = StorageService(
+    name=MOCK_STORAGE_SERVICE_NAME,
+    url="http://example.com",
+    user_name="test",
+    api_key="test",
+    download_limit=20,
+    download_offset=10,
+    default=False,
+)
+
+MOCK_AIP_NAME = "Test transfer"
+MOCK_AIP_UUID = uuid.uuid4()
+TEST_AIP = AIP(
+    uuid=MOCK_AIP_UUID,
+    transfer_name=MOCK_AIP_NAME,
+    create_date=datetime.datetime.now(),
+    storage_service_id=MOCK_STORAGE_SERVICE_ID,
+    fetch_job_id=1,
+)
+
+
+
[docs]@pytest.mark.parametrize( + "file_data, file_count", [([], 0), (TEST_FILES, 3), (TEST_FILES[:2], 2)] +) +def test_largest_files(app_instance, mocker, file_data, file_count): + """Test that return value conforms to expected structure. + """ + mock_query = mocker.patch("AIPscan.Data.data._largest_files_query") + mock_query.return_value = file_data + + mock_get_ss = mocker.patch("AIPscan.Data.data._get_storage_service") + mock_get_ss.return_value = TEST_STORAGE_SERVICE + + mock_get_aip = mocker.patch("sqlalchemy.orm.query.Query.get") + mock_get_aip.return_value = TEST_AIP + + report = data.largest_files(MOCK_STORAGE_SERVICE_ID) + report_files = report[data.FIELD_FILES] + assert report[data.FIELD_STORAGE_NAME] == MOCK_STORAGE_SERVICE_NAME + assert len(report_files) == file_count
+ + +
[docs]@pytest.mark.parametrize( + "test_file, has_format_version, has_puid", + [ + (TEST_FILES[0], False, False), + (TEST_FILES[1], False, True), + (TEST_FILES[2], True, False), + ], +) +def test_largest_files_elements( + app_instance, mocker, test_file, has_format_version, has_puid +): + """Test that returned file data matches expected values. + """ + mock_query = mocker.patch("AIPscan.Data.data._largest_files_query") + mock_query.return_value = [test_file] + + mock_get_ss = mocker.patch("AIPscan.Data.data._get_storage_service") + mock_get_ss.return_value = TEST_STORAGE_SERVICE + + mock_get_aip = mocker.patch("sqlalchemy.orm.query.Query.get") + mock_get_aip.return_value = TEST_AIP + + report = data.largest_files(MOCK_STORAGE_SERVICE_ID) + report_file = report[data.FIELD_FILES][0] + + # Required elements + assert test_file.name == report_file.get(data.FIELD_NAME) + assert test_file.file_format == report_file.get(data.FIELD_FORMAT) + + # Optional elements + if has_format_version: + assert test_file.format_version == report_file.get(data.FIELD_VERSION) + else: + assert report_file.get(data.FIELD_VERSION) is None + + if has_puid: + assert test_file.puid == report_file.get(data.FIELD_PUID) + else: + assert report_file.get(data.FIELD_PUID) is None + + # AIP information + assert report_file.get(data.FIELD_AIP_NAME) == MOCK_AIP_NAME + assert report_file.get(data.FIELD_AIP_UUID) == MOCK_AIP_UUID
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Home/views.html b/docs/_build/html/_modules/AIPscan/Home/views.html new file mode 100644 index 00000000..96c91ef4 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Home/views.html @@ -0,0 +1,123 @@ + + + + + + + + AIPscan.Home.views — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Home.views

+# -*- coding: utf-8 -*-
+
+from flask import Blueprint, redirect, url_for
+
+home = Blueprint("home", __name__)
+
+
+
[docs]@home.route("/", methods=["GET"]) +def index(): + """Define handling for application's / route.""" + return redirect(url_for("aggregator.ss_default"))
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Reporter/helpers.html b/docs/_build/html/_modules/AIPscan/Reporter/helpers.html new file mode 100644 index 00000000..488ded57 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Reporter/helpers.html @@ -0,0 +1,151 @@ + + + + + + + + AIPscan.Reporter.helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Reporter.helpers

+# -*- coding: utf-8 -*-
+
+"""Code shared across reporting modules but not outside of reporting.
+"""
+
+from AIPscan.Data import data
+
+
+
[docs]def translate_headers(headers): + """Translate headers from something machine readable to something + more user friendly and translatable. + """ + field_lookup = { + data.FIELD_AIP_NAME: "AIP Name", + data.FIELD_AIP: "AIP", + data.FIELD_AIPS: "AIPs", + data.FIELD_AIP_SIZE: "AIP Size", + data.FIELD_ALL_AIPS: "All AIPs", + data.FIELD_COUNT: "Count", + data.FIELD_CREATED_DATE: "Created Date", + data.FIELD_DERIVATIVE_COUNT: "Derivative Count", + data.FIELD_DERIVATIVE_FORMAT: "Derivative Format", + data.FIELD_DERIVATIVE_UUID: "Derivative UUID", + data.FIELD_FILE_COUNT: "File Count", + data.FIELD_FILE_TYPE: "Type", + data.FIELD_FILENAME: "Filename", + data.FIELD_FORMAT: "Format", + data.FIELD_FORMATS: "Formats", + data.FIELD_NAME: "Name", + data.FIELD_ORIGINAL_UUID: "Original UUID", + data.FIELD_ORIGINAL_FORMAT: "Original Format", + data.FIELD_PUID: "PUID", + data.FIELD_RELATED_PAIRING: "Related Pairing", + data.FIELD_SIZE: "Size", + data.FIELD_STORAGE_NAME: "Storage Service Name", + data.FIELD_TRANSFER_NAME: "Transfer Name", + data.FIELD_VERSION: "Version", + } + return [field_lookup.get(header, header) for header in headers]
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Reporter/report_aip_contents.html b/docs/_build/html/_modules/AIPscan/Reporter/report_aip_contents.html new file mode 100644 index 00000000..ad6c5b7a --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Reporter/report_aip_contents.html @@ -0,0 +1,166 @@ + + + + + + + + AIPscan.Reporter.report_aip_contents — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Reporter.report_aip_contents

+# -*- coding: utf-8 -*-
+
+from flask import render_template, request
+
+from AIPscan.Data import data
+from AIPscan.helpers import get_human_readable_file_size
+from AIPscan.Reporter import reporter, translate_headers
+
+
+
[docs]@reporter.route("/aip_contents/", methods=["GET"]) +def aip_contents(): + """Return AIP contents organized by format.""" + storage_service_id = request.args.get("amss_id") + aip_data = data.aip_overview_two(storage_service_id=storage_service_id) + FIELD_UUID = "UUID" + headers = [ + FIELD_UUID, + data.FIELD_AIP_NAME, + data.FIELD_CREATED_DATE, + data.FIELD_AIP_SIZE, + ] + format_lookup = aip_data[data.FIELD_FORMATS] + format_headers = list(aip_data[data.FIELD_FORMATS].keys()) + storage_service_name = aip_data[data.FIELD_STORAGE_NAME] + aip_data.pop(data.FIELD_FORMATS, None) + aip_data.pop(data.FIELD_STORAGE_NAME, None) + rows = [] + for k, v in aip_data.items(): + row = [] + for header in headers: + if header == FIELD_UUID: + row.append(k) + elif header == data.FIELD_AIP_SIZE: + row.append(get_human_readable_file_size(v.get(header))) + elif header != data.FIELD_FORMATS: + row.append(v.get(header)) + formats = v.get(data.FIELD_FORMATS) + for format_header in format_headers: + format_ = formats.get(format_header) + count = 0 + if format_: + count = format_.get(data.FIELD_COUNT, 0) + row.append(count) + rows.append(row) + headers = headers + format_headers + return render_template( + "aip_contents.html", + storage_service=storage_service_id, + storage_service_name=storage_service_name, + aip_data=aip_data, + columns=translate_headers(headers), + rows=rows, + format_lookup=format_lookup, + )
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Reporter/report_formats_count.html b/docs/_build/html/_modules/AIPscan/Reporter/report_formats_count.html new file mode 100644 index 00000000..7ae099f5 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Reporter/report_formats_count.html @@ -0,0 +1,343 @@ + + + + + + + + AIPscan.Reporter.report_formats_count — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Reporter.report_formats_count

+# -*- coding: utf-8 -*-
+
+"""Report formats count consists of the tabular report, plot, and
+chart which describe the file formats present across the AIPs in a
+storage service with AIPs filtered by date range.
+"""
+
+from collections import Counter
+from datetime import datetime, timedelta
+
+from flask import render_template, request
+
+from AIPscan.models import AIP, Event, File, FileType, StorageService
+from AIPscan.helpers import get_human_readable_file_size
+from AIPscan.Reporter import reporter
+
+
+
[docs]@reporter.route("/report_formats_count/", methods=["GET"]) +def report_formats_count(): + """Report (tabular) on all file formats and their counts and size on + disk across all AIPs in the storage service. + """ + start_date = request.args.get("startdate") + end_date = request.args.get("enddate") + # make date range inclusive + start = datetime.strptime(start_date, "%Y-%m-%d") + end = datetime.strptime(end_date, "%Y-%m-%d") + day_before = start - timedelta(days=1) + day_after = end + timedelta(days=1) + + storage_service_id = request.args.get("ssId") + storage_service = StorageService.query.get(storage_service_id) + aips = AIP.query.filter_by(storage_service_id=storage_service_id).all() + + format_count = {} + originals_count = 0 + + for aip in aips: + originals_count += aip.original_file_count + original_files = File.query.filter_by( + aip_id=aip.id, file_type=FileType.original + ) + for original in original_files: + # Note that original files in packages do not have a PREMIS ingestion + # event. Therefore "message digest calculation" is used to get the + # ingest date for all originals. This event typically happens within + # the same second or seconds of the ingestion event and is done for all files. + ingest_event = Event.query.filter_by( + file_id=original.id, type="message digest calculation" + ).first() + if ingest_event.date < day_before: + continue + elif ingest_event.date > day_after: + continue + else: + file_format = original.file_format + size = original.size + + if file_format in format_count: + format_count[file_format]["count"] += 1 + if format_count[file_format]["size"] is not None: + format_count[file_format]["size"] += size + else: + format_count.update({file_format: {"count": 1, "size": size}}) + + total_size = 0 + + for key, value in format_count.items(): + size = value["size"] + if size is not None: + total_size += size + human_size = get_human_readable_file_size(size) + format_count[key] = { + "count": value["count"], + "size": size, + "humansize": human_size, + } + + different_formats = len(format_count.keys()) + total_human_size = get_human_readable_file_size(total_size) + + return render_template( + "report_formats_count.html", + startdate=start_date, + enddate=end_date, + storageService=storage_service, + originalsCount=originals_count, + formatCount=format_count, + differentFormats=different_formats, + totalHumanSize=total_human_size, + )
+ + +
[docs]@reporter.route("/chart_formats_count/", methods=["GET"]) +def chart_formats_count(): + """Report (pie chart) on all file formats and their counts and size + on disk across all AIPs in the storage service.""" + start_date = request.args.get("startdate") + end_date = request.args.get("enddate") + # make date range inclusive + start = datetime.strptime(start_date, "%Y-%m-%d") + end = datetime.strptime(end_date, "%Y-%m-%d") + day_before = start - timedelta(days=1) + day_after = end + timedelta(days=1) + + storage_service_id = request.args.get("ssId") + storage_service = StorageService.query.get(storage_service_id) + aips = AIP.query.filter_by(storage_service_id=storage_service_id).all() + + format_labels = [] + format_counts = [] + originals_count = 0 + + for aip in aips: + originals_count += aip.original_file_count + original_files = File.query.filter_by( + aip_id=aip.id, file_type=FileType.original + ) + for original in original_files: + # Note that original files in packages do not have a PREMIS ingestion + # event. Therefore "message digest calculation" is used to get the + # ingest date for all originals. This event typically happens within + # the same second or seconds of the ingestion event and is done for all files. + ingest_event = Event.query.filter_by( + file_id=original.id, type="message digest calculation" + ).first() + if ingest_event.date < day_before: + continue + elif ingest_event.date > day_after: + continue + else: + format_labels.append(original.file_format) + + format_counts = Counter(format_labels) + labels = list(format_counts.keys()) + values = list(format_counts.values()) + + different_formats = len(format_counts.keys()) + + return render_template( + "chart_formats_count.html", + startdate=start_date, + enddate=end_date, + storageService=storage_service, + labels=labels, + values=values, + originalsCount=originals_count, + differentFormats=different_formats, + )
+ + +
[docs]@reporter.route("/plot_formats_count/", methods=["GET"]) +def plot_formats_count(): + """Report (scatter) on all file formats and their counts and size on + disk across all AIPs in the storage service. + """ + start_date = request.args.get("startdate") + end_date = request.args.get("enddate") + # make date range inclusive + start = datetime.strptime(start_date, "%Y-%m-%d") + end = datetime.strptime(end_date, "%Y-%m-%d") + day_before = start - timedelta(days=1) + day_after = end + timedelta(days=1) + + storage_service_id = request.args.get("ssId") + storage_service = StorageService.query.get(storage_service_id) + aips = AIP.query.filter_by(storage_service_id=storage_service_id).all() + + format_count = {} + originals_count = 0 + + for aip in aips: + originals_count += aip.original_file_count + original_files = File.query.filter_by( + aip_id=aip.id, file_type=FileType.original + ) + for original in original_files: + # Note that original files in packages do not have a PREMIS ingestion + # event. Therefore "message digest calculation" is used to get the + # ingest date for all originals. This event typically happens within + # the same second or seconds of the ingestion event and is done for all files. + ingest_event = Event.query.filter_by( + file_id=original.id, type="message digest calculation" + ).first() + if ingest_event.date < day_before: + continue + elif ingest_event.date > day_after: + continue + else: + file_format = original.file_format + size = original.size + + if file_format in format_count: + format_count[file_format]["count"] += 1 + format_count[file_format]["size"] += size + else: + format_count.update({file_format: {"count": 1, "size": size}}) + + total_size = 0 + x_axis = [] + y_axis = [] + file_format = [] + human_size = [] + + for _, value in format_count.items(): + y_axis.append(value["count"]) + size = value["size"] + if size is None: + size = 0 + x_axis.append(size) + total_size += size + human_size.append(get_human_readable_file_size(size)) + + file_format = list(format_count.keys()) + different_formats = len(format_count.keys()) + total_human_size = get_human_readable_file_size(total_size) + + return render_template( + "plot_formats_count.html", + startdate=start_date, + enddate=end_date, + storageService=storage_service, + originalsCount=originals_count, + formatCount=format_count, + differentFormats=different_formats, + totalHumanSize=total_human_size, + x_axis=x_axis, + y_axis=y_axis, + format=file_format, + humansize=human_size, + )
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Reporter/report_largest_files.html b/docs/_build/html/_modules/AIPscan/Reporter/report_largest_files.html new file mode 100644 index 00000000..293b7535 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Reporter/report_largest_files.html @@ -0,0 +1,152 @@ + + + + + + + + AIPscan.Reporter.report_largest_files — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Reporter.report_largest_files

+# -*- coding: utf-8 -*-
+
+from flask import render_template, request
+
+from AIPscan.Data import data
+from AIPscan.Reporter import reporter, translate_headers
+
+
+
[docs]@reporter.route("/largest_files/", methods=["GET"]) +def largest_files(): + """Return largest files.""" + storage_service_id = request.args.get("amss_id") + file_type = request.args.get("file_type") + limit = 20 + try: + limit = int(request.args.get("limit", 20)) + except ValueError: + pass + # TODO: Make limit configurable - currently set to default of 20 + file_data = data.largest_files( + storage_service_id=storage_service_id, file_type=file_type, limit=limit + ) + storage_service_name = file_data[data.FIELD_STORAGE_NAME] + headers = [ + data.FIELD_FILENAME, + data.FIELD_SIZE, + data.FIELD_FORMAT, + data.FIELD_PUID, + data.FIELD_FILE_TYPE, + data.FIELD_AIP, + ] + return render_template( + "report_largest_files.html", + storage_service_id=storage_service_id, + storage_service_name=storage_service_name, + columns=translate_headers(headers), + files=file_data[data.FIELD_FILES], + file_type=file_type, + limit=limit, + )
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Reporter/report_originals_with_derivatives.html b/docs/_build/html/_modules/AIPscan/Reporter/report_originals_with_derivatives.html new file mode 100644 index 00000000..0add2886 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Reporter/report_originals_with_derivatives.html @@ -0,0 +1,162 @@ + + + + + + + + AIPscan.Reporter.report_originals_with_derivatives — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Reporter.report_originals_with_derivatives

+# -*- coding: utf-8 -*-
+
+"""Report on original copies that have a preservation derivative and
+the different file formats associated with both.
+"""
+
+from flask import render_template, request
+
+from AIPscan.Data import data
+from AIPscan.Reporter import reporter, translate_headers
+
+
+
[docs]@reporter.route("/original_derivatives/", methods=["GET"]) +def original_derivatives(): + """Return a mapping between original files and derivatives if they + exist. + """ + aips_with_preservation_files = [] + storage_service_id = request.args.get("amss_id") + derivative_data = data.derivative_overview(storage_service_id=storage_service_id) + storage_service_name = derivative_data[data.FIELD_STORAGE_NAME] + + for aip in derivative_data[data.FIELD_ALL_AIPS]: + aip_info = {} + aip_info[data.FIELD_TRANSFER_NAME] = aip[data.FIELD_TRANSFER_NAME] + aip_info[data.FIELD_UUID] = aip[data.FIELD_UUID] + aip_info[data.FIELD_DERIVATIVE_COUNT] = aip[data.FIELD_DERIVATIVE_COUNT] + aip_info["table_data"] = [] + for pairing in aip[data.FIELD_RELATED_PAIRING]: + row = [] + row.append(pairing[data.FIELD_ORIGINAL_UUID]) + row.append(pairing[data.FIELD_ORIGINAL_FORMAT]) + row.append(pairing[data.FIELD_DERIVATIVE_UUID]) + row.append(pairing[data.FIELD_DERIVATIVE_FORMAT]) + aip_info["table_data"].append(row) + aips_with_preservation_files.append(aip_info) + headers = [ + data.FIELD_ORIGINAL_UUID, + data.FIELD_ORIGINAL_FORMAT, + data.FIELD_DERIVATIVE_UUID, + data.FIELD_DERIVATIVE_FORMAT, + ] + return render_template( + "report_originals_derivatives.html", + storage_service=storage_service_id, + storage_service_name=storage_service_name, + aip_count=len(aips_with_preservation_files), + headers=translate_headers(headers), + aips=aips_with_preservation_files, + )
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/Reporter/views.html b/docs/_build/html/_modules/AIPscan/Reporter/views.html new file mode 100644 index 00000000..ecd8bce9 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/Reporter/views.html @@ -0,0 +1,260 @@ + + + + + + + + AIPscan.Reporter.views — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.Reporter.views

+# -*- coding: utf-8 -*-
+
+"""Views contains the primary routes for navigation around AIPscan's
+Reporter module. Reports themselves as siphoned off into separate module
+files with singular responsibility for a report.
+"""
+
+from datetime import datetime
+
+from flask import render_template
+
+from AIPscan.models import AIP, File, FileType, Event, FetchJob, StorageService
+from AIPscan.Reporter import reporter
+
+# Flask's idiom requires code using routing decorators to be imported
+# up-front. But that means it might not be called directly by a module.
+from AIPscan.Reporter import (  # noqa: F401
+    report_aip_contents,
+    report_formats_count,
+    report_originals_with_derivatives,
+    report_largest_files,
+)
+
+
+
[docs]@reporter.route("/aips/", methods=["GET"]) +@reporter.route("/aips/<storage_service_id>", methods=["GET"]) +def view_aips(storage_service_id=0): + """View aips returns a standard page in AIPscan that provides an + overview of the AIPs in a given storage service. + """ + DEFAULT_STORAGE_SERVICE_ID = 1 + storage_services = {} + storage_id = int(storage_service_id) + if storage_id == 0 or storage_id is None: + storage_id = DEFAULT_STORAGE_SERVICE_ID + storage_service = StorageService.query.get(storage_id) + if storage_service: + aips = AIP.query.filter_by(storage_service_id=storage_service.id).all() + aips_list = [] + for aip in aips: + aip_info = {} + aip_info["id"] = aip.id + aip_info["transfer_name"] = aip.transfer_name + aip_info["uuid"] = aip.uuid + aip_info["create_date"] = aip.create_date + aip_info["originals_count"] = aip.original_file_count + aip_info["copies_count"] = aip.preservation_file_count + aips_list.append(aip_info) + + aips_count = len(aips) + storage_services = StorageService.query.all() + else: + aips_list = [] + aips_count = 0 + return render_template( + "aips.html", + storage_services=storage_services, + storage_service_id=storage_id, + aips_count=aips_count, + aips=aips_list, + )
+ + +# Picturae TODO: Does this work with AIP UUID as well? +
[docs]@reporter.route("/aip/<aip_id>", methods=["GET"]) +def view_aip(aip_id): + """View aip returns a standard page in AIPscan that provides a more + detailed view of a specific AIP given an AIPs ID. + """ + aip = AIP.query.get(aip_id) + fetch_job = FetchJob.query.get(aip.fetch_job_id) + storage_service = StorageService.query.get(fetch_job.storage_service_id) + aips_count = AIP.query.filter_by(storage_service_id=storage_service.id).count() + original_file_count = aip.original_file_count + preservation_file_count = aip.preservation_file_count + originals = [] + original_files = File.query.filter_by( + aip_id=aip.id, file_type=FileType.original + ).all() + for file_ in original_files: + original = {} + original["id"] = file_.id + original["name"] = file_.name + original["uuid"] = file_.uuid + original["size"] = file_.size + original["date_created"] = file_.date_created.strftime("%Y-%m-%d") + original["puid"] = file_.puid + original["file_format"] = file_.file_format + original["format_version"] = file_.format_version + preservation_file = File.query.filter_by( + file_type=FileType.preservation, original_file_id=file_.id + ).first() + if preservation_file is not None: + original["preservation_file_id"] = preservation_file.id + originals.append(original) + + return render_template( + "aip.html", + aip=aip, + storage_service=storage_service, + aips_count=aips_count, + originals=originals, + original_file_count=original_file_count, + preservation_file_count=preservation_file_count, + )
+ + +
[docs]@reporter.route("/file/<file_id>", methods=["GET"]) +def view_file(file_id): + """File page displays Object and Event metadata for file + """ + file_ = File.query.get(file_id) + aip = AIP.query.get(file_.aip_id) + events = Event.query.filter_by(file_id=file_id).all() + preservation_file = File.query.filter_by( + file_type=FileType.preservation, original_file_id=file_.id + ).first() + + original_filename = None + if file_.original_file_id is not None: + original_file = File.query.get(file_.original_file_id) + original_filename = original_file.name + + return render_template( + "file.html", + file_=file_, + aip=aip, + events=events, + preservation_file=preservation_file, + original_filename=original_filename, + )
+ + +
[docs]@reporter.route("/reports/", methods=["GET"]) +def reports(): + """Reports returns a standard page in AIPscan that lists the + in-built reports available to the caller. + """ + all_storage_services = StorageService.query.all() + now = datetime.now() + start_date = str(datetime(now.year, 1, 1))[:-9] + end_date = str(datetime(now.year, now.month, now.day))[:-9] + return render_template( + "reports.html", + storage_services=all_storage_services, + start_date=start_date, + end_date=end_date, + )
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/User/forms.html b/docs/_build/html/_modules/AIPscan/User/forms.html new file mode 100644 index 00000000..a32c8a60 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/User/forms.html @@ -0,0 +1,124 @@ + + + + + + + + AIPscan.User.forms — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.User.forms

+# -*- coding: utf-8 -*-
+
+from flask_wtf import FlaskForm
+from wtforms import StringField, PasswordField, BooleanField, SubmitField
+from wtforms.validators import DataRequired
+
+
+
[docs]class LoginForm(FlaskForm): + username = StringField("Username", validators=[DataRequired()]) + password = PasswordField("Password", validators=[DataRequired()]) + remember_me = BooleanField("Remember Me") + submit = SubmitField("Sign In")
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/celery.html b/docs/_build/html/_modules/AIPscan/celery.html new file mode 100644 index 00000000..1a9c313f --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/celery.html @@ -0,0 +1,132 @@ + + + + + + + + AIPscan.celery — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.celery

+# -*- coding: utf-8 -*-
+
+"""This module contains code related to Celery configuration."""
+
+from AIPscan import extensions
+
+
+
[docs]def configure_celery(app): + """Add Flask app context to celery.Task.""" + TaskBase = extensions.celery.Task + + class ContextTask(TaskBase): + abstract = True + + def __call__(self, *args, **kwargs): + with app.app_context(): + return TaskBase.__call__(self, *args, **kwargs) + + extensions.celery.Task = ContextTask + return extensions.celery
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/conftest.html b/docs/_build/html/_modules/AIPscan/conftest.html new file mode 100644 index 00000000..07e8862f --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/conftest.html @@ -0,0 +1,136 @@ + + + + + + + + AIPscan.conftest — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.conftest

+# -*- coding: utf-8 -*-
+
+"""This module defines shared AIPscan pytest fixtures."""
+
+import pytest
+
+from AIPscan import db, create_app
+
+
+
[docs]@pytest.fixture +def app_instance(): + """Pytest fixture that returns an instance of our application. + + This fixture provides a Flask application context for tests using + AIPscan's test configuration. + + This pattern can be extended in additional fixtures to, e.g. load + state to the test database from a fixture as needed for tests. + """ + app = create_app("test") + with app.app_context(): + db.create_all() + yield app + db.drop_all()
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/helpers.html b/docs/_build/html/_modules/AIPscan/helpers.html new file mode 100644 index 00000000..56377575 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/helpers.html @@ -0,0 +1,122 @@ + + + + + + + + AIPscan.helpers — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.helpers

+# -*- coding: utf-8 -*-
+
+
+
[docs]def get_human_readable_file_size(size, precision=2): + suffixes = ["B", "KiB", "MiB", "GiB", "TiB"] + suffixIndex = 0 + while size > 1024 and suffixIndex < 4: + suffixIndex += 1 # increment the index of the suffix + size = size / 1024.0 # apply the division + return "%.*f %s" % (precision, size, suffixes[suffixIndex])
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/AIPscan/models.html b/docs/_build/html/_modules/AIPscan/models.html new file mode 100644 index 00000000..9a61a7d7 --- /dev/null +++ b/docs/_build/html/_modules/AIPscan/models.html @@ -0,0 +1,350 @@ + + + + + + + + AIPscan.models — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for AIPscan.models

+# -*- coding: utf-8 -*-
+import enum
+
+from AIPscan import db
+
+
+
[docs]class package_tasks(db.Model): + __bind_key__ = "celery" + package_task_id = db.Column(db.String(36), primary_key=True) + workflow_coordinator_id = db.Column(db.String(36))
+ + +
[docs]class get_mets_tasks(db.Model): + __bind_key__ = "celery" + get_mets_task_id = db.Column(db.String(36), primary_key=True) + workflow_coordinator_id = db.Column(db.String(36)) + package_uuid = db.Column(db.String(36)) + status = db.Column(db.String())
+ + +
[docs]class StorageService(db.Model): + __tablename__ = "storage_service" + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(255), index=True, unique=True) + url = db.Column(db.String(255)) + user_name = db.Column(db.String(255)) + api_key = db.Column(db.String(255)) + download_limit = db.Column(db.Integer()) + download_offset = db.Column(db.Integer()) + default = db.Column(db.Boolean) + fetch_jobs = db.relationship( + "FetchJob", cascade="all,delete", backref="storage_service", lazy=True + ) + + def __init__( + self, name, url, user_name, api_key, download_limit, download_offset, default + ): + self.name = name + self.url = url + self.user_name = user_name + self.api_key = api_key + self.download_limit = download_limit + self.download_offset = download_offset + self.default = default + + def __repr__(self): + return "<Storage Service '{}'>".format(self.name)
+ + +
[docs]class FetchJob(db.Model): + __tablename__ = "fetch_job" + id = db.Column(db.Integer(), primary_key=True) + total_packages = db.Column(db.Integer()) + total_aips = db.Column(db.Integer()) + total_dips = db.Column(db.Integer()) + total_sips = db.Column(db.Integer()) + total_replicas = db.Column(db.Integer()) + total_deleted_aips = db.Column(db.Integer()) + download_start = db.Column(db.DateTime()) + download_end = db.Column(db.DateTime()) + download_directory = db.Column(db.String(255)) + storage_service_id = db.Column( + db.Integer(), db.ForeignKey("storage_service.id"), nullable=False + ) + aips = db.relationship("AIP", cascade="all,delete", backref="fetch_job", lazy=True) + + def __init__( + self, + total_packages, + total_aips, + total_deleted_aips, + download_start, + download_end, + download_directory, + storage_service_id, + ): + self.total_packages = total_packages + self.total_aips = total_aips + self.total_deleted_aips = total_deleted_aips + self.download_start = download_start + self.download_end = download_end + self.download_directory = download_directory + self.storage_service_id = storage_service_id + + def __repr__(self): + return "<Fetch Job '{}'>".format(self.download_start)
+ + +
[docs]class AIP(db.Model): + __tablename__ = "aip" + id = db.Column(db.Integer(), primary_key=True) + uuid = db.Column(db.String(255), index=True) + transfer_name = db.Column(db.String(255)) + create_date = db.Column(db.DateTime()) + storage_service_id = db.Column( + db.Integer(), db.ForeignKey("storage_service.id"), nullable=False + ) + fetch_job_id = db.Column( + db.Integer(), db.ForeignKey("fetch_job.id"), nullable=False + ) + files = db.relationship("File", cascade="all,delete", backref="aip", lazy=True) + + def __init__( + self, uuid, transfer_name, create_date, storage_service_id, fetch_job_id + ): + self.uuid = uuid + self.transfer_name = transfer_name + self.create_date = create_date + self.storage_service_id = storage_service_id + self.fetch_job_id = fetch_job_id + + def __repr__(self): + return "<AIP '{}'>".format(self.transfer_name) + + @property + def original_file_count(self): + return File.query.filter_by(aip_id=self.id, file_type=FileType.original).count() + + @property + def preservation_file_count(self): + return File.query.filter_by( + aip_id=self.id, file_type=FileType.preservation + ).count()
+ + +
[docs]class FileType(enum.Enum): + original = "original" + preservation = "preservation"
+ + +
[docs]class File(db.Model): + __tablename__ = "file" + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(255), index=True) + filepath = db.Column(db.Text(), nullable=True) # Accommodate long filepaths. + uuid = db.Column(db.String(255), index=True) + file_type = db.Column(db.Enum(FileType)) + size = db.Column(db.Integer()) + # Date created maps to PREMIS dateCreatedByApplication for original + # files, which in practice is almost always date last modified, and + # to normalization date for preservation files. + date_created = db.Column(db.DateTime()) + puid = db.Column(db.String(255), index=True) + file_format = db.Column(db.String(255)) + format_version = db.Column(db.String(255)) + checksum_type = db.Column(db.String(255)) + checksum_value = db.Column(db.String(255), index=True) + + original_file_id = db.Column(db.Integer(), db.ForeignKey("file.id")) + original_file = db.relationship( + "File", remote_side=[id], backref=db.backref("derivatives") + ) + + aip_id = db.Column(db.Integer(), db.ForeignKey("aip.id"), nullable=False) + events = db.relationship("Event", cascade="all,delete", backref="file", lazy=True) + + def __init__( + self, + name, + filepath, + uuid, + size, + date_created, + file_format, + checksum_type, + checksum_value, + aip_id, + file_type=FileType.original, + format_version=None, + puid=None, + original_file_id=None, + ): + self.name = name + self.filepath = filepath + self.uuid = uuid + self.file_type = file_type + self.size = size + self.date_created = date_created + self.puid = puid + self.file_format = file_format + self.format_version = format_version + self.checksum_type = checksum_type + self.checksum_value = checksum_value + self.original_file_id = original_file_id + self.aip_id = aip_id + + def __repr__(self): + return "<File '{}' - '{}'".format(self.id, self.name)
+ + +EventAgent = db.Table( + "event_agents", + db.Column("event_id", db.Integer, db.ForeignKey("event.id")), + db.Column("agent_id", db.Integer, db.ForeignKey("agent.id")), +) + + +
[docs]class Event(db.Model): + __tablename__ = "event" + id = db.Column(db.Integer(), primary_key=True) + type = db.Column(db.String(255), index=True) + uuid = db.Column(db.String(255), index=True) + date = db.Column(db.DateTime()) + detail = db.Column(db.String(255)) + outcome = db.Column(db.String(255)) + outcome_detail = db.Column(db.String(255)) + file_id = db.Column(db.Integer(), db.ForeignKey("file.id"), nullable=False) + event_agents = db.relationship( + "Agent", secondary=EventAgent, backref=db.backref("Event", lazy="dynamic") + ) + + def __init__(self, type, uuid, date, detail, outcome, outcome_detail, file_id): + self.type = type + self.uuid = uuid + self.date = date + self.detail = detail + self.outcome = outcome + self.outcome_detail = outcome_detail + self.file_id = file_id + + def __repr__(self): + return "<Event '{}'>".format(self.type)
+ + +
[docs]class Agent(db.Model): + __tablename__ = "agent" + id = db.Column(db.Integer(), primary_key=True) + linking_type_value = db.Column(db.String(255), index=True) + agent_type = db.Column(db.String(255), index=True) + agent_value = db.Column(db.String(255), index=True) + + def __init__(self, linking_type_value, agent_type, agent_value): + self.linking_type_value = linking_type_value + self.agent_type = agent_type + self.agent_value = agent_value + + def __repr__(self): + return "<Agent '{}: {}'>".format(self.agent_type, self.agent_value)
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/flask_restx/api.html b/docs/_build/html/_modules/flask_restx/api.html new file mode 100644 index 00000000..74a1029f --- /dev/null +++ b/docs/_build/html/_modules/flask_restx/api.html @@ -0,0 +1,1037 @@ + + + + + + + + flask_restx.api — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for flask_restx.api

+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import difflib
+import inspect
+from itertools import chain
+import logging
+import operator
+import re
+import six
+import sys
+
+from collections import OrderedDict
+from functools import wraps, partial
+from types import MethodType
+
+from flask import url_for, request, current_app
+from flask import make_response as original_flask_make_response
+from flask.helpers import _endpoint_from_view_func
+from flask.signals import got_request_exception
+
+from jsonschema import RefResolver
+
+from werkzeug.utils import cached_property
+from werkzeug.datastructures import Headers
+from werkzeug.exceptions import (
+    HTTPException,
+    MethodNotAllowed,
+    NotFound,
+    NotAcceptable,
+    InternalServerError,
+)
+from werkzeug.wrappers import BaseResponse
+
+from . import apidoc
+from .mask import ParseError, MaskError
+from .namespace import Namespace
+from .postman import PostmanCollectionV1
+from .resource import Resource
+from .swagger import Swagger
+from .utils import default_id, camel_to_dash, unpack
+from .representations import output_json
+from ._http import HTTPStatus
+
+RE_RULES = re.compile("(<.*>)")
+
+# List headers that should never be handled by Flask-RESTX
+HEADERS_BLACKLIST = ("Content-Length",)
+
+DEFAULT_REPRESENTATIONS = [("application/json", output_json)]
+
+log = logging.getLogger(__name__)
+
+
+class Api(object):
+    """
+    The main entry point for the application.
+    You need to initialize it with a Flask Application: ::
+
+    >>> app = Flask(__name__)
+    >>> api = Api(app)
+
+    Alternatively, you can use :meth:`init_app` to set the Flask application
+    after it has been constructed.
+
+    The endpoint parameter prefix all views and resources:
+
+        - The API root/documentation will be ``{endpoint}.root``
+        - A resource registered as 'resource' will be available as ``{endpoint}.resource``
+
+    :param flask.Flask|flask.Blueprint app: the Flask application object or a Blueprint
+    :param str version: The API version (used in Swagger documentation)
+    :param str title: The API title (used in Swagger documentation)
+    :param str description: The API description (used in Swagger documentation)
+    :param str terms_url: The API terms page URL (used in Swagger documentation)
+    :param str contact: A contact email for the API (used in Swagger documentation)
+    :param str license: The license associated to the API (used in Swagger documentation)
+    :param str license_url: The license page URL (used in Swagger documentation)
+    :param str endpoint: The API base endpoint (default to 'api).
+    :param str default: The default namespace base name (default to 'default')
+    :param str default_label: The default namespace label (used in Swagger documentation)
+    :param str default_mediatype: The default media type to return
+    :param bool validate: Whether or not the API should perform input payload validation.
+    :param bool ordered: Whether or not preserve order models and marshalling.
+    :param str doc: The documentation path. If set to a false value, documentation is disabled.
+                (Default to '/')
+    :param list decorators: Decorators to attach to every resource
+    :param bool catch_all_404s: Use :meth:`handle_error`
+        to handle 404 errors throughout your app
+    :param dict authorizations: A Swagger Authorizations declaration as dictionary
+    :param bool serve_challenge_on_401: Serve basic authentication challenge with 401
+        responses (default 'False')
+    :param FormatChecker format_checker: A jsonschema.FormatChecker object that is hooked into
+        the Model validator. A default or a custom FormatChecker can be provided (e.g., with custom
+        checkers), otherwise the default action is to not enforce any format validation.
+    """
+
+    def __init__(
+        self,
+        app=None,
+        version="1.0",
+        title=None,
+        description=None,
+        terms_url=None,
+        license=None,
+        license_url=None,
+        contact=None,
+        contact_url=None,
+        contact_email=None,
+        authorizations=None,
+        security=None,
+        doc="/",
+        default_id=default_id,
+        default="default",
+        default_label="Default namespace",
+        validate=None,
+        tags=None,
+        prefix="",
+        ordered=False,
+        default_mediatype="application/json",
+        decorators=None,
+        catch_all_404s=False,
+        serve_challenge_on_401=False,
+        format_checker=None,
+        **kwargs
+    ):
+        self.version = version
+        self.title = title or "API"
+        self.description = description
+        self.terms_url = terms_url
+        self.contact = contact
+        self.contact_email = contact_email
+        self.contact_url = contact_url
+        self.license = license
+        self.license_url = license_url
+        self.authorizations = authorizations
+        self.security = security
+        self.default_id = default_id
+        self.ordered = ordered
+        self._validate = validate
+        self._doc = doc
+        self._doc_view = None
+        self._default_error_handler = None
+        self.tags = tags or []
+
+        self.error_handlers = {
+            ParseError: mask_parse_error_handler,
+            MaskError: mask_error_handler,
+        }
+        self._schema = None
+        self.models = {}
+        self._refresolver = None
+        self.format_checker = format_checker
+        self.namespaces = []
+
+        self.ns_paths = dict()
+
+        self.representations = OrderedDict(DEFAULT_REPRESENTATIONS)
+        self.urls = {}
+        self.prefix = prefix
+        self.default_mediatype = default_mediatype
+        self.decorators = decorators if decorators else []
+        self.catch_all_404s = catch_all_404s
+        self.serve_challenge_on_401 = serve_challenge_on_401
+        self.blueprint_setup = None
+        self.endpoints = set()
+        self.resources = []
+        self.app = None
+        self.blueprint = None
+        # must come after self.app initialisation to prevent __getattr__ recursion
+        # in self._configure_namespace_logger
+        self.default_namespace = self.namespace(
+            default,
+            default_label,
+            endpoint="{0}-declaration".format(default),
+            validate=validate,
+            api=self,
+            path="/",
+        )
+        if app is not None:
+            self.app = app
+            self.init_app(app)
+        # super(Api, self).__init__(app, **kwargs)
+
+    def init_app(self, app, **kwargs):
+        """
+        Allow to lazy register the API on a Flask application::
+
+        >>> app = Flask(__name__)
+        >>> api = Api()
+        >>> api.init_app(app)
+
+        :param flask.Flask app: the Flask application object
+        :param str title: The API title (used in Swagger documentation)
+        :param str description: The API description (used in Swagger documentation)
+        :param str terms_url: The API terms page URL (used in Swagger documentation)
+        :param str contact: A contact email for the API (used in Swagger documentation)
+        :param str license: The license associated to the API (used in Swagger documentation)
+        :param str license_url: The license page URL (used in Swagger documentation)
+
+        """
+        self.app = app
+        self.title = kwargs.get("title", self.title)
+        self.description = kwargs.get("description", self.description)
+        self.terms_url = kwargs.get("terms_url", self.terms_url)
+        self.contact = kwargs.get("contact", self.contact)
+        self.contact_url = kwargs.get("contact_url", self.contact_url)
+        self.contact_email = kwargs.get("contact_email", self.contact_email)
+        self.license = kwargs.get("license", self.license)
+        self.license_url = kwargs.get("license_url", self.license_url)
+        self._add_specs = kwargs.get("add_specs", True)
+
+        # If app is a blueprint, defer the initialization
+        try:
+            app.record(self._deferred_blueprint_init)
+        # Flask.Blueprint has a 'record' attribute, Flask.Api does not
+        except AttributeError:
+            self._init_app(app)
+        else:
+            self.blueprint = app
+
+    def _init_app(self, app):
+        """
+        Perform initialization actions with the given :class:`flask.Flask` object.
+
+        :param flask.Flask app: The flask application object
+        """
+        self._register_specs(self.blueprint or app)
+        self._register_doc(self.blueprint or app)
+
+        app.handle_exception = partial(self.error_router, app.handle_exception)
+        app.handle_user_exception = partial(
+            self.error_router, app.handle_user_exception
+        )
+
+        if len(self.resources) > 0:
+            for resource, namespace, urls, kwargs in self.resources:
+                self._register_view(app, resource, namespace, *urls, **kwargs)
+
+        for ns in self.namespaces:
+            self._configure_namespace_logger(app, ns)
+
+        self._register_apidoc(app)
+        self._validate = (
+            self._validate
+            if self._validate is not None
+            else app.config.get("RESTX_VALIDATE", False)
+        )
+        app.config.setdefault("RESTX_MASK_HEADER", "X-Fields")
+        app.config.setdefault("RESTX_MASK_SWAGGER", True)
+
+    def __getattr__(self, name):
+        try:
+            return getattr(self.default_namespace, name)
+        except AttributeError:
+            raise AttributeError("Api does not have {0} attribute".format(name))
+
+    def _complete_url(self, url_part, registration_prefix):
+        """
+        This method is used to defer the construction of the final url in
+        the case that the Api is created with a Blueprint.
+
+        :param url_part: The part of the url the endpoint is registered with
+        :param registration_prefix: The part of the url contributed by the
+            blueprint.  Generally speaking, BlueprintSetupState.url_prefix
+        """
+        parts = (registration_prefix, self.prefix, url_part)
+        return "".join(part for part in parts if part)
+
+    def _register_apidoc(self, app):
+        conf = app.extensions.setdefault("restx", {})
+        if not conf.get("apidoc_registered", False):
+            app.register_blueprint(apidoc.apidoc)
+        conf["apidoc_registered"] = True
+
+    def _register_specs(self, app_or_blueprint):
+        if self._add_specs:
+            endpoint = str("specs")
+            self._register_view(
+                app_or_blueprint,
+                SwaggerView,
+                self.default_namespace,
+                "/swagger.json",
+                endpoint=endpoint,
+                resource_class_args=(self,),
+            )
+            self.endpoints.add(endpoint)
+
+    def _register_doc(self, app_or_blueprint):
+        if self._add_specs and self._doc:
+            # Register documentation before root if enabled
+            app_or_blueprint.add_url_rule(self._doc, "doc", self.render_doc)
+        app_or_blueprint.add_url_rule(self.prefix or "/", "root", self.render_root)
+
+    def register_resource(self, namespace, resource, *urls, **kwargs):
+        endpoint = kwargs.pop("endpoint", None)
+        endpoint = str(endpoint or self.default_endpoint(resource, namespace))
+
+        kwargs["endpoint"] = endpoint
+        self.endpoints.add(endpoint)
+
+        if self.app is not None:
+            self._register_view(self.app, resource, namespace, *urls, **kwargs)
+        else:
+            self.resources.append((resource, namespace, urls, kwargs))
+        return endpoint
+
+    def _configure_namespace_logger(self, app, namespace):
+        for handler in app.logger.handlers:
+            namespace.logger.addHandler(handler)
+        namespace.logger.setLevel(app.logger.level)
+
+    def _register_view(self, app, resource, namespace, *urls, **kwargs):
+        endpoint = kwargs.pop("endpoint", None) or camel_to_dash(resource.__name__)
+        resource_class_args = kwargs.pop("resource_class_args", ())
+        resource_class_kwargs = kwargs.pop("resource_class_kwargs", {})
+
+        # NOTE: 'view_functions' is cleaned up from Blueprint class in Flask 1.0
+        if endpoint in getattr(app, "view_functions", {}):
+            previous_view_class = app.view_functions[endpoint].__dict__["view_class"]
+
+            # if you override the endpoint with a different class, avoid the
+            # collision by raising an exception
+            if previous_view_class != resource:
+                msg = "This endpoint (%s) is already set to the class %s."
+                raise ValueError(msg % (endpoint, previous_view_class.__name__))
+
+        resource.mediatypes = self.mediatypes_method()  # Hacky
+        resource.endpoint = endpoint
+
+        resource_func = self.output(
+            resource.as_view(
+                endpoint, self, *resource_class_args, **resource_class_kwargs
+            )
+        )
+
+        # Apply Namespace and Api decorators to a resource
+        for decorator in chain(namespace.decorators, self.decorators):
+            resource_func = decorator(resource_func)
+
+        for url in urls:
+            # If this Api has a blueprint
+            if self.blueprint:
+                # And this Api has been setup
+                if self.blueprint_setup:
+                    # Set the rule to a string directly, as the blueprint is already
+                    # set up.
+                    self.blueprint_setup.add_url_rule(
+                        url, view_func=resource_func, **kwargs
+                    )
+                    continue
+                else:
+                    # Set the rule to a function that expects the blueprint prefix
+                    # to construct the final url.  Allows deferment of url finalization
+                    # in the case that the associated Blueprint has not yet been
+                    # registered to an application, so we can wait for the registration
+                    # prefix
+                    rule = partial(self._complete_url, url)
+            else:
+                # If we've got no Blueprint, just build a url with no prefix
+                rule = self._complete_url(url, "")
+            # Add the url to the application or blueprint
+            app.add_url_rule(rule, view_func=resource_func, **kwargs)
+
+    def output(self, resource):
+        """
+        Wraps a resource (as a flask view function),
+        for cases where the resource does not directly return a response object
+
+        :param resource: The resource as a flask view function
+        """
+
+        @wraps(resource)
+        def wrapper(*args, **kwargs):
+            resp = resource(*args, **kwargs)
+            if isinstance(resp, BaseResponse):
+                return resp
+            data, code, headers = unpack(resp)
+            return self.make_response(data, code, headers=headers)
+
+        return wrapper
+
+    def make_response(self, data, *args, **kwargs):
+        """
+        Looks up the representation transformer for the requested media
+        type, invoking the transformer to create a response object. This
+        defaults to default_mediatype if no transformer is found for the
+        requested mediatype. If default_mediatype is None, a 406 Not
+        Acceptable response will be sent as per RFC 2616 section 14.1
+
+        :param data: Python object containing response data to be transformed
+        """
+        default_mediatype = (
+            kwargs.pop("fallback_mediatype", None) or self.default_mediatype
+        )
+        mediatype = request.accept_mimetypes.best_match(
+            self.representations, default=default_mediatype,
+        )
+        if mediatype is None:
+            raise NotAcceptable()
+        if mediatype in self.representations:
+            resp = self.representations[mediatype](data, *args, **kwargs)
+            resp.headers["Content-Type"] = mediatype
+            return resp
+        elif mediatype == "text/plain":
+            resp = original_flask_make_response(str(data), *args, **kwargs)
+            resp.headers["Content-Type"] = "text/plain"
+            return resp
+        else:
+            raise InternalServerError()
+
+    def documentation(self, func):
+        """A decorator to specify a view function for the documentation"""
+        self._doc_view = func
+        return func
+
+    def render_root(self):
+        self.abort(HTTPStatus.NOT_FOUND)
+
+    def render_doc(self):
+        """Override this method to customize the documentation page"""
+        if self._doc_view:
+            return self._doc_view()
+        elif not self._doc:
+            self.abort(HTTPStatus.NOT_FOUND)
+        return apidoc.ui_for(self)
+
+    def default_endpoint(self, resource, namespace):
+        """
+        Provide a default endpoint for a resource on a given namespace.
+
+        Endpoints are ensured not to collide.
+
+        Override this method specify a custom algorithm for default endpoint.
+
+        :param Resource resource: the resource for which we want an endpoint
+        :param Namespace namespace: the namespace holding the resource
+        :returns str: An endpoint name
+        """
+        endpoint = camel_to_dash(resource.__name__)
+        if namespace is not self.default_namespace:
+            endpoint = "{ns.name}_{endpoint}".format(ns=namespace, endpoint=endpoint)
+        if endpoint in self.endpoints:
+            suffix = 2
+            while True:
+                new_endpoint = "{base}_{suffix}".format(base=endpoint, suffix=suffix)
+                if new_endpoint not in self.endpoints:
+                    endpoint = new_endpoint
+                    break
+                suffix += 1
+        return endpoint
+
+    def get_ns_path(self, ns):
+        return self.ns_paths.get(ns)
+
+    def ns_urls(self, ns, urls):
+        path = self.get_ns_path(ns) or ns.path
+        return [path + url for url in urls]
+
+    def add_namespace(self, ns, path=None):
+        """
+        This method registers resources from namespace for current instance of api.
+        You can use argument path for definition custom prefix url for namespace.
+
+        :param Namespace ns: the namespace
+        :param path: registration prefix of namespace
+        """
+        if ns not in self.namespaces:
+            self.namespaces.append(ns)
+            if self not in ns.apis:
+                ns.apis.append(self)
+            # Associate ns with prefix-path
+            if path is not None:
+                self.ns_paths[ns] = path
+        # Register resources
+        for r in ns.resources:
+            urls = self.ns_urls(ns, r.urls)
+            self.register_resource(ns, r.resource, *urls, **r.kwargs)
+        # Register models
+        for name, definition in six.iteritems(ns.models):
+            self.models[name] = definition
+        if not self.blueprint and self.app is not None:
+            self._configure_namespace_logger(self.app, ns)
+
+    def namespace(self, *args, **kwargs):
+        """
+        A namespace factory.
+
+        :returns Namespace: a new namespace instance
+        """
+        kwargs["ordered"] = kwargs.get("ordered", self.ordered)
+        ns = Namespace(*args, **kwargs)
+        self.add_namespace(ns)
+        return ns
+
+    def endpoint(self, name):
+        if self.blueprint:
+            return "{0}.{1}".format(self.blueprint.name, name)
+        else:
+            return name
+
+    @property
+    def specs_url(self):
+        """
+        The Swagger specifications absolute url (ie. `swagger.json`)
+
+        :rtype: str
+        """
+        return url_for(self.endpoint("specs"), _external=True)
+
+    @property
+    def base_url(self):
+        """
+        The API base absolute url
+
+        :rtype: str
+        """
+        return url_for(self.endpoint("root"), _external=True)
+
+    @property
+    def base_path(self):
+        """
+        The API path
+
+        :rtype: str
+        """
+        return url_for(self.endpoint("root"), _external=False)
+
+    @cached_property
+    def __schema__(self):
+        """
+        The Swagger specifications/schema for this API
+
+        :returns dict: the schema as a serializable dict
+        """
+        if not self._schema:
+            try:
+                self._schema = Swagger(self).as_dict()
+            except Exception:
+                # Log the source exception for debugging purpose
+                # and return an error message
+                msg = "Unable to render schema"
+                log.exception(msg)  # This will provide a full traceback
+                return {"error": msg}
+        return self._schema
+
+    @property
+    def _own_and_child_error_handlers(self):
+        rv = {}
+        rv.update(self.error_handlers)
+        for ns in self.namespaces:
+            for exception, handler in six.iteritems(ns.error_handlers):
+                rv[exception] = handler
+        return rv
+
+    def errorhandler(self, exception):
+        """A decorator to register an error handler for a given exception"""
+        if inspect.isclass(exception) and issubclass(exception, Exception):
+            # Register an error handler for a given exception
+            def wrapper(func):
+                self.error_handlers[exception] = func
+                return func
+
+            return wrapper
+        else:
+            # Register the default error handler
+            self._default_error_handler = exception
+            return exception
+
+    def owns_endpoint(self, endpoint):
+        """
+        Tests if an endpoint name (not path) belongs to this Api.
+        Takes into account the Blueprint name part of the endpoint name.
+
+        :param str endpoint: The name of the endpoint being checked
+        :return: bool
+        """
+
+        if self.blueprint:
+            if endpoint.startswith(self.blueprint.name):
+                endpoint = endpoint.split(self.blueprint.name + ".", 1)[-1]
+            else:
+                return False
+        return endpoint in self.endpoints
+
+    def _should_use_fr_error_handler(self):
+        """
+        Determine if error should be handled with FR or default Flask
+
+        The goal is to return Flask error handlers for non-FR-related routes,
+        and FR errors (with the correct media type) for FR endpoints. This
+        method currently handles 404 and 405 errors.
+
+        :return: bool
+        """
+        adapter = current_app.create_url_adapter(request)
+
+        try:
+            adapter.match()
+        except MethodNotAllowed as e:
+            # Check if the other HTTP methods at this url would hit the Api
+            valid_route_method = e.valid_methods[0]
+            rule, _ = adapter.match(method=valid_route_method, return_rule=True)
+            return self.owns_endpoint(rule.endpoint)
+        except NotFound:
+            return self.catch_all_404s
+        except Exception:
+            # Werkzeug throws other kinds of exceptions, such as Redirect
+            pass
+
+    def _has_fr_route(self):
+        """Encapsulating the rules for whether the request was to a Flask endpoint"""
+        # 404's, 405's, which might not have a url_rule
+        if self._should_use_fr_error_handler():
+            return True
+        # for all other errors, just check if FR dispatched the route
+        if not request.url_rule:
+            return False
+        return self.owns_endpoint(request.url_rule.endpoint)
+
+    def error_router(self, original_handler, e):
+        """
+        This function decides whether the error occurred in a flask-restx
+        endpoint or not. If it happened in a flask-restx endpoint, our
+        handler will be dispatched. If it happened in an unrelated view, the
+        app's original error handler will be dispatched.
+        In the event that the error occurred in a flask-restx endpoint but
+        the local handler can't resolve the situation, the router will fall
+        back onto the original_handler as last resort.
+
+        :param function original_handler: the original Flask error handler for the app
+        :param Exception e: the exception raised while handling the request
+        """
+        if self._has_fr_route():
+            try:
+                return self.handle_error(e)
+            except Exception as f:
+                return original_handler(f)
+        return original_handler(e)
+
+    def handle_error(self, e):
+        """
+        Error handler for the API transforms a raised exception into a Flask response,
+        with the appropriate HTTP status code and body.
+
+        :param Exception e: the raised Exception object
+
+        """
+        got_request_exception.send(current_app._get_current_object(), exception=e)
+
+        # When propagate_exceptions is set, do not return the exception to the
+        # client if a handler is configured for the exception.
+        if (
+            not isinstance(e, HTTPException)
+            and current_app.propagate_exceptions
+            and not isinstance(e, tuple(self.error_handlers.keys()))
+        ):
+
+            exc_type, exc_value, tb = sys.exc_info()
+            if exc_value is e:
+                raise
+            else:
+                raise e
+
+        include_message_in_response = current_app.config.get(
+            "ERROR_INCLUDE_MESSAGE", True
+        )
+        default_data = {}
+
+        headers = Headers()
+
+        for typecheck, handler in six.iteritems(self._own_and_child_error_handlers):
+            if isinstance(e, typecheck):
+                result = handler(e)
+                default_data, code, headers = unpack(
+                    result, HTTPStatus.INTERNAL_SERVER_ERROR
+                )
+                break
+        else:
+            if isinstance(e, HTTPException):
+                code = HTTPStatus(e.code)
+                if include_message_in_response:
+                    default_data = {"message": getattr(e, "description", code.phrase)}
+                headers = e.get_response().headers
+            elif self._default_error_handler:
+                result = self._default_error_handler(e)
+                default_data, code, headers = unpack(
+                    result, HTTPStatus.INTERNAL_SERVER_ERROR
+                )
+            else:
+                code = HTTPStatus.INTERNAL_SERVER_ERROR
+                if include_message_in_response:
+                    default_data = {
+                        "message": code.phrase,
+                    }
+
+        if include_message_in_response:
+            default_data["message"] = default_data.get("message", str(e))
+
+        data = getattr(e, "data", default_data)
+        fallback_mediatype = None
+
+        if code >= HTTPStatus.INTERNAL_SERVER_ERROR:
+            exc_info = sys.exc_info()
+            if exc_info[1] is None:
+                exc_info = None
+            current_app.log_exception(exc_info)
+
+        elif (
+            code == HTTPStatus.NOT_FOUND
+            and current_app.config.get("ERROR_404_HELP", True)
+            and include_message_in_response
+        ):
+            data["message"] = self._help_on_404(data.get("message", None))
+
+        elif code == HTTPStatus.NOT_ACCEPTABLE and self.default_mediatype is None:
+            # if we are handling NotAcceptable (406), make sure that
+            # make_response uses a representation we support as the
+            # default mediatype (so that make_response doesn't throw
+            # another NotAcceptable error).
+            supported_mediatypes = list(self.representations.keys())
+            fallback_mediatype = (
+                supported_mediatypes[0] if supported_mediatypes else "text/plain"
+            )
+
+        # Remove blacklisted headers
+        for header in HEADERS_BLACKLIST:
+            headers.pop(header, None)
+
+        resp = self.make_response(
+            data, code, headers, fallback_mediatype=fallback_mediatype
+        )
+
+        if code == HTTPStatus.UNAUTHORIZED:
+            resp = self.unauthorized(resp)
+        return resp
+
+    def _help_on_404(self, message=None):
+        rules = dict(
+            [
+                (RE_RULES.sub("", rule.rule), rule.rule)
+                for rule in current_app.url_map.iter_rules()
+            ]
+        )
+        close_matches = difflib.get_close_matches(request.path, rules.keys())
+        if close_matches:
+            # If we already have a message, add punctuation and continue it.
+            message = "".join(
+                (
+                    (message.rstrip(".") + ". ") if message else "",
+                    "You have requested this URI [",
+                    request.path,
+                    "] but did you mean ",
+                    " or ".join((rules[match] for match in close_matches)),
+                    " ?",
+                )
+            )
+        return message
+
+    def as_postman(self, urlvars=False, swagger=False):
+        """
+        Serialize the API as Postman collection (v1)
+
+        :param bool urlvars: whether to include or not placeholders for query strings
+        :param bool swagger: whether to include or not the swagger.json specifications
+
+        """
+        return PostmanCollectionV1(self, swagger=swagger).as_dict(urlvars=urlvars)
+
+    @property
+    def payload(self):
+        """Store the input payload in the current request context"""
+        return request.get_json()
+
+    @property
+    def refresolver(self):
+        if not self._refresolver:
+            self._refresolver = RefResolver.from_schema(self.__schema__)
+        return self._refresolver
+
+    @staticmethod
+    def _blueprint_setup_add_url_rule_patch(
+        blueprint_setup, rule, endpoint=None, view_func=None, **options
+    ):
+        """
+        Method used to patch BlueprintSetupState.add_url_rule for setup
+        state instance corresponding to this Api instance.  Exists primarily
+        to enable _complete_url's function.
+
+        :param blueprint_setup: The BlueprintSetupState instance (self)
+        :param rule: A string or callable that takes a string and returns a
+            string(_complete_url) that is the url rule for the endpoint
+            being registered
+        :param endpoint: See BlueprintSetupState.add_url_rule
+        :param view_func: See BlueprintSetupState.add_url_rule
+        :param **options: See BlueprintSetupState.add_url_rule
+        """
+
+        if callable(rule):
+            rule = rule(blueprint_setup.url_prefix)
+        elif blueprint_setup.url_prefix:
+            rule = blueprint_setup.url_prefix + rule
+        options.setdefault("subdomain", blueprint_setup.subdomain)
+        if endpoint is None:
+            endpoint = _endpoint_from_view_func(view_func)
+        defaults = blueprint_setup.url_defaults
+        if "defaults" in options:
+            defaults = dict(defaults, **options.pop("defaults"))
+        blueprint_setup.app.add_url_rule(
+            rule,
+            "%s.%s" % (blueprint_setup.blueprint.name, endpoint),
+            view_func,
+            defaults=defaults,
+            **options
+        )
+
+    def _deferred_blueprint_init(self, setup_state):
+        """
+        Synchronize prefix between blueprint/api and registration options, then
+        perform initialization with setup_state.app :class:`flask.Flask` object.
+        When a :class:`flask_restx.Api` object is initialized with a blueprint,
+        this method is recorded on the blueprint to be run when the blueprint is later
+        registered to a :class:`flask.Flask` object.  This method also monkeypatches
+        BlueprintSetupState.add_url_rule with _blueprint_setup_add_url_rule_patch.
+
+        :param setup_state: The setup state object passed to deferred functions
+            during blueprint registration
+        :type setup_state: flask.blueprints.BlueprintSetupState
+
+        """
+
+        self.blueprint_setup = setup_state
+        if setup_state.add_url_rule.__name__ != "_blueprint_setup_add_url_rule_patch":
+            setup_state._original_add_url_rule = setup_state.add_url_rule
+            setup_state.add_url_rule = MethodType(
+                Api._blueprint_setup_add_url_rule_patch, setup_state
+            )
+        if not setup_state.first_registration:
+            raise ValueError("flask-restx blueprints can only be registered once.")
+        self._init_app(setup_state.app)
+
+    def mediatypes_method(self):
+        """Return a method that returns a list of mediatypes"""
+        return lambda resource_cls: self.mediatypes() + [self.default_mediatype]
+
+    def mediatypes(self):
+        """Returns a list of requested mediatypes sent in the Accept header"""
+        return [
+            h
+            for h, q in sorted(
+                request.accept_mimetypes, key=operator.itemgetter(1), reverse=True
+            )
+        ]
+
+    def representation(self, mediatype):
+        """
+        Allows additional representation transformers to be declared for the
+        api. Transformers are functions that must be decorated with this
+        method, passing the mediatype the transformer represents. Three
+        arguments are passed to the transformer:
+
+        * The data to be represented in the response body
+        * The http status code
+        * A dictionary of headers
+
+        The transformer should convert the data appropriately for the mediatype
+        and return a Flask response object.
+
+        Ex::
+
+            @api.representation('application/xml')
+            def xml(data, code, headers):
+                resp = make_response(convert_data_to_xml(data), code)
+                resp.headers.extend(headers)
+                return resp
+        """
+
+        def wrapper(func):
+            self.representations[mediatype] = func
+            return func
+
+        return wrapper
+
+    def unauthorized(self, response):
+        """Given a response, change it to ask for credentials"""
+
+        if self.serve_challenge_on_401:
+            realm = current_app.config.get("HTTP_BASIC_AUTH_REALM", "flask-restx")
+            challenge = '{0} realm="{1}"'.format("Basic", realm)
+
+            response.headers["WWW-Authenticate"] = challenge
+        return response
+
+    def url_for(self, resource, **values):
+        """
+        Generates a URL to the given resource.
+
+        Works like :func:`flask.url_for`.
+        """
+        endpoint = resource.endpoint
+        if self.blueprint:
+            endpoint = "{0}.{1}".format(self.blueprint.name, endpoint)
+        return url_for(endpoint, **values)
+
+
+class SwaggerView(Resource):
+    """Render the Swagger specifications as JSON"""
+
+    def get(self):
+        schema = self.api.__schema__
+        return (
+            schema,
+            HTTPStatus.INTERNAL_SERVER_ERROR if "error" in schema else HTTPStatus.OK,
+        )
+
+    def mediatypes(self):
+        return ["application/json"]
+
+
+def mask_parse_error_handler(error):
+    """When a mask can't be parsed"""
+    return {"message": "Mask parse error: {0}".format(error)}, HTTPStatus.BAD_REQUEST
+
+
+def mask_error_handler(error):
+    """When any error occurs on mask"""
+    return {"message": "Mask error: {0}".format(error)}, HTTPStatus.BAD_REQUEST
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/flask_restx/namespace.html b/docs/_build/html/_modules/flask_restx/namespace.html new file mode 100644 index 00000000..96f5a20b --- /dev/null +++ b/docs/_build/html/_modules/flask_restx/namespace.html @@ -0,0 +1,486 @@ + + + + + + + + flask_restx.namespace — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for flask_restx.namespace

+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import inspect
+import warnings
+import logging
+from collections import namedtuple
+
+import six
+from flask import request
+from flask.views import http_method_funcs
+
+from ._http import HTTPStatus
+from .errors import abort
+from .marshalling import marshal, marshal_with
+from .model import Model, OrderedModel, SchemaModel
+from .reqparse import RequestParser
+from .utils import merge
+
+# Container for each route applied to a Resource using @ns.route decorator
+ResourceRoute = namedtuple("ResourceRoute", "resource urls route_doc kwargs")
+
+
+class Namespace(object):
+    """
+    Group resources together.
+
+    Namespace is to API what :class:`flask:flask.Blueprint` is for :class:`flask:flask.Flask`.
+
+    :param str name: The namespace name
+    :param str description: An optional short description
+    :param str path: An optional prefix path. If not provided, prefix is ``/+name``
+    :param list decorators: A list of decorators to apply to each resources
+    :param bool validate: Whether or not to perform validation on this namespace
+    :param bool ordered: Whether or not to preserve order on models and marshalling
+    :param Api api: an optional API to attache to the namespace
+    """
+
+    def __init__(
+        self,
+        name,
+        description=None,
+        path=None,
+        decorators=None,
+        validate=None,
+        authorizations=None,
+        ordered=False,
+        **kwargs
+    ):
+        self.name = name
+        self.description = description
+        self._path = path
+
+        self._schema = None
+        self._validate = validate
+        self.models = {}
+        self.urls = {}
+        self.decorators = decorators if decorators else []
+        self.resources = []  # List[ResourceRoute]
+        self.error_handlers = {}
+        self.default_error_handler = None
+        self.authorizations = authorizations
+        self.ordered = ordered
+        self.apis = []
+        if "api" in kwargs:
+            self.apis.append(kwargs["api"])
+        self.logger = logging.getLogger(__name__ + "." + self.name)
+
+    @property
+    def path(self):
+        return (self._path or ("/" + self.name)).rstrip("/")
+
+    def add_resource(self, resource, *urls, **kwargs):
+        """
+        Register a Resource for a given API Namespace
+
+        :param Resource resource: the resource ro register
+        :param str urls: one or more url routes to match for the resource,
+                         standard flask routing rules apply.
+                         Any url variables will be passed to the resource method as args.
+        :param str endpoint: endpoint name (defaults to :meth:`Resource.__name__.lower`
+            Can be used to reference this route in :class:`fields.Url` fields
+        :param list|tuple resource_class_args: args to be forwarded to the constructor of the resource.
+        :param dict resource_class_kwargs: kwargs to be forwarded to the constructor of the resource.
+
+        Additional keyword arguments not specified above will be passed as-is
+        to :meth:`flask.Flask.add_url_rule`.
+
+        Examples::
+
+            namespace.add_resource(HelloWorld, '/', '/hello')
+            namespace.add_resource(Foo, '/foo', endpoint="foo")
+            namespace.add_resource(FooSpecial, '/special/foo', endpoint="foo")
+        """
+        route_doc = kwargs.pop("route_doc", {})
+        self.resources.append(ResourceRoute(resource, urls, route_doc, kwargs))
+        for api in self.apis:
+            ns_urls = api.ns_urls(self, urls)
+            api.register_resource(self, resource, *ns_urls, **kwargs)
+
+    def route(self, *urls, **kwargs):
+        """
+        A decorator to route resources.
+        """
+
+        def wrapper(cls):
+            doc = kwargs.pop("doc", None)
+            if doc is not None:
+                # build api doc intended only for this route
+                kwargs["route_doc"] = self._build_doc(cls, doc)
+            self.add_resource(cls, *urls, **kwargs)
+            return cls
+
+        return wrapper
+
+    def _build_doc(self, cls, doc):
+        if doc is False:
+            return False
+        unshortcut_params_description(doc)
+        handle_deprecations(doc)
+        for http_method in http_method_funcs:
+            if http_method in doc:
+                if doc[http_method] is False:
+                    continue
+                unshortcut_params_description(doc[http_method])
+                handle_deprecations(doc[http_method])
+                if "expect" in doc[http_method] and not isinstance(
+                    doc[http_method]["expect"], (list, tuple)
+                ):
+                    doc[http_method]["expect"] = [doc[http_method]["expect"]]
+        return merge(getattr(cls, "__apidoc__", {}), doc)
+
+    def doc(self, shortcut=None, **kwargs):
+        """A decorator to add some api documentation to the decorated object"""
+        if isinstance(shortcut, six.text_type):
+            kwargs["id"] = shortcut
+        show = shortcut if isinstance(shortcut, bool) else True
+
+        def wrapper(documented):
+            documented.__apidoc__ = self._build_doc(
+                documented, kwargs if show else False
+            )
+            return documented
+
+        return wrapper
+
+    def hide(self, func):
+        """A decorator to hide a resource or a method from specifications"""
+        return self.doc(False)(func)
+
+    def abort(self, *args, **kwargs):
+        """
+        Properly abort the current request
+
+        See: :func:`~flask_restx.errors.abort`
+        """
+        abort(*args, **kwargs)
+
+    def add_model(self, name, definition):
+        self.models[name] = definition
+        for api in self.apis:
+            api.models[name] = definition
+        return definition
+
+    def model(self, name=None, model=None, mask=None, **kwargs):
+        """
+        Register a model
+
+        .. seealso:: :class:`Model`
+        """
+        cls = OrderedModel if self.ordered else Model
+        model = cls(name, model, mask=mask)
+        model.__apidoc__.update(kwargs)
+        return self.add_model(name, model)
+
+    def schema_model(self, name=None, schema=None):
+        """
+        Register a model
+
+        .. seealso:: :class:`Model`
+        """
+        model = SchemaModel(name, schema)
+        return self.add_model(name, model)
+
+    def extend(self, name, parent, fields):
+        """
+        Extend a model (Duplicate all fields)
+
+        :deprecated: since 0.9. Use :meth:`clone` instead
+        """
+        if isinstance(parent, list):
+            parents = parent + [fields]
+            model = Model.extend(name, *parents)
+        else:
+            model = Model.extend(name, parent, fields)
+        return self.add_model(name, model)
+
+    def clone(self, name, *specs):
+        """
+        Clone a model (Duplicate all fields)
+
+        :param str name: the resulting model name
+        :param specs: a list of models from which to clone the fields
+
+        .. seealso:: :meth:`Model.clone`
+
+        """
+        model = Model.clone(name, *specs)
+        return self.add_model(name, model)
+
+    def inherit(self, name, *specs):
+        """
+        Inherit a model (use the Swagger composition pattern aka. allOf)
+
+        .. seealso:: :meth:`Model.inherit`
+        """
+        model = Model.inherit(name, *specs)
+        return self.add_model(name, model)
+
+    def expect(self, *inputs, **kwargs):
+        """
+        A decorator to Specify the expected input model
+
+        :param ModelBase|Parse inputs: An expect model or request parser
+        :param bool validate: whether to perform validation or not
+
+        """
+        expect = []
+        params = {"validate": kwargs.get("validate", self._validate), "expect": expect}
+        for param in inputs:
+            expect.append(param)
+        return self.doc(**params)
+
+    def parser(self):
+        """Instanciate a :class:`~RequestParser`"""
+        return RequestParser()
+
+    def as_list(self, field):
+        """Allow to specify nested lists for documentation"""
+        field.__apidoc__ = merge(getattr(field, "__apidoc__", {}), {"as_list": True})
+        return field
+
+    def marshal_with(
+        self, fields, as_list=False, code=HTTPStatus.OK, description=None, **kwargs
+    ):
+        """
+        A decorator specifying the fields to use for serialization.
+
+        :param bool as_list: Indicate that the return type is a list (for the documentation)
+        :param int code: Optionally give the expected HTTP response code if its different from 200
+
+        """
+
+        def wrapper(func):
+            doc = {
+                "responses": {
+                    str(code): (description, [fields], kwargs)
+                    if as_list
+                    else (description, fields, kwargs)
+                },
+                "__mask__": kwargs.get(
+                    "mask", True
+                ),  # Mask values can't be determined outside app context
+            }
+            func.__apidoc__ = merge(getattr(func, "__apidoc__", {}), doc)
+            return marshal_with(fields, ordered=self.ordered, **kwargs)(func)
+
+        return wrapper
+
+    def marshal_list_with(self, fields, **kwargs):
+        """A shortcut decorator for :meth:`~Api.marshal_with` with ``as_list=True``"""
+        return self.marshal_with(fields, True, **kwargs)
+
+    def marshal(self, *args, **kwargs):
+        """A shortcut to the :func:`marshal` helper"""
+        return marshal(*args, **kwargs)
+
+    def errorhandler(self, exception):
+        """A decorator to register an error handler for a given exception"""
+        if inspect.isclass(exception) and issubclass(exception, Exception):
+            # Register an error handler for a given exception
+            def wrapper(func):
+                self.error_handlers[exception] = func
+                return func
+
+            return wrapper
+        else:
+            # Register the default error handler
+            self.default_error_handler = exception
+            return exception
+
+    def param(self, name, description=None, _in="query", **kwargs):
+        """
+        A decorator to specify one of the expected parameters
+
+        :param str name: the parameter name
+        :param str description: a small description
+        :param str _in: the parameter location `(query|header|formData|body|cookie)`
+        """
+        param = kwargs
+        param["in"] = _in
+        param["description"] = description
+        return self.doc(params={name: param})
+
+    def response(self, code, description, model=None, **kwargs):
+        """
+        A decorator to specify one of the expected responses
+
+        :param int code: the HTTP status code
+        :param str description: a small description about the response
+        :param ModelBase model: an optional response model
+
+        """
+        return self.doc(responses={str(code): (description, model, kwargs)})
+
+    def header(self, name, description=None, **kwargs):
+        """
+        A decorator to specify one of the expected headers
+
+        :param str name: the HTTP header name
+        :param str description: a description about the header
+
+        """
+        header = {"description": description}
+        header.update(kwargs)
+        return self.doc(headers={name: header})
+
+    def produces(self, mimetypes):
+        """A decorator to specify the MIME types the API can produce"""
+        return self.doc(produces=mimetypes)
+
+    def deprecated(self, func):
+        """A decorator to mark a resource or a method as deprecated"""
+        return self.doc(deprecated=True)(func)
+
+    def vendor(self, *args, **kwargs):
+        """
+        A decorator to expose vendor extensions.
+
+        Extensions can be submitted as dict or kwargs.
+        The ``x-`` prefix is optionnal and will be added if missing.
+
+        See: http://swagger.io/specification/#specification-extensions-128
+        """
+        for arg in args:
+            kwargs.update(arg)
+        return self.doc(vendor=kwargs)
+
+    @property
+    def payload(self):
+        """Store the input payload in the current request context"""
+        return request.get_json()
+
+
+def unshortcut_params_description(data):
+    if "params" in data:
+        for name, description in six.iteritems(data["params"]):
+            if isinstance(description, six.string_types):
+                data["params"][name] = {"description": description}
+
+
+def handle_deprecations(doc):
+    if "parser" in doc:
+        warnings.warn(
+            "The parser attribute is deprecated, use expect instead",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        doc["expect"] = doc.get("expect", []) + [doc.pop("parser")]
+    if "body" in doc:
+        warnings.warn(
+            "The body attribute is deprecated, use expect instead",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        doc["expect"] = doc.get("expect", []) + [doc.pop("body")]
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/index.html b/docs/_build/html/_modules/index.html new file mode 100644 index 00000000..cc1b3338 --- /dev/null +++ b/docs/_build/html/_modules/index.html @@ -0,0 +1,141 @@ + + + + + + + + Overview: module code — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/sqlalchemy/orm/attributes.html b/docs/_build/html/_modules/sqlalchemy/orm/attributes.html new file mode 100644 index 00000000..12012cb9 --- /dev/null +++ b/docs/_build/html/_modules/sqlalchemy/orm/attributes.html @@ -0,0 +1,2159 @@ + + + + + + + + sqlalchemy.orm.attributes — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for sqlalchemy.orm.attributes

+# orm/attributes.py
+# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""Defines instrumentation for class attributes and their interaction
+with instances.
+
+This module is usually not directly visible to user applications, but
+defines a large part of the ORM's interactivity.
+
+
+"""
+
+import operator
+
+from . import collections
+from . import exc as orm_exc
+from . import interfaces
+from .base import ATTR_EMPTY
+from .base import ATTR_WAS_SET
+from .base import CALLABLES_OK
+from .base import INIT_OK
+from .base import instance_dict
+from .base import instance_state
+from .base import instance_str
+from .base import LOAD_AGAINST_COMMITTED
+from .base import manager_of_class
+from .base import NEVER_SET
+from .base import NO_AUTOFLUSH
+from .base import NO_CHANGE  # noqa
+from .base import NO_RAISE
+from .base import NO_VALUE
+from .base import NON_PERSISTENT_OK  # noqa
+from .base import PASSIVE_NO_FETCH
+from .base import PASSIVE_NO_FETCH_RELATED  # noqa
+from .base import PASSIVE_NO_INITIALIZE
+from .base import PASSIVE_NO_RESULT
+from .base import PASSIVE_OFF
+from .base import PASSIVE_ONLY_PERSISTENT
+from .base import PASSIVE_RETURN_NEVER_SET
+from .base import RELATED_OBJECT_OK  # noqa
+from .base import SQL_OK  # noqa
+from .base import state_str
+from .. import event
+from .. import inspection
+from .. import util
+
+
+@inspection._self_inspects
+class QueryableAttribute(
+    interfaces._MappedAttribute,
+    interfaces.InspectionAttr,
+    interfaces.PropComparator,
+):
+    """Base class for :term:`descriptor` objects that intercept
+    attribute events on behalf of a :class:`.MapperProperty`
+    object.  The actual :class:`.MapperProperty` is accessible
+    via the :attr:`.QueryableAttribute.property`
+    attribute.
+
+
+    .. seealso::
+
+        :class:`.InstrumentedAttribute`
+
+        :class:`.MapperProperty`
+
+        :attr:`.Mapper.all_orm_descriptors`
+
+        :attr:`.Mapper.attrs`
+    """
+
+    is_attribute = True
+
+    def __init__(
+        self,
+        class_,
+        key,
+        impl=None,
+        comparator=None,
+        parententity=None,
+        of_type=None,
+    ):
+        self.class_ = class_
+        self.key = key
+        self.impl = impl
+        self.comparator = comparator
+        self._parententity = parententity
+        self._of_type = of_type
+
+        manager = manager_of_class(class_)
+        # manager is None in the case of AliasedClass
+        if manager:
+            # propagate existing event listeners from
+            # immediate superclass
+            for base in manager._bases:
+                if key in base:
+                    self.dispatch._update(base[key].dispatch)
+                    if base[key].dispatch._active_history:
+                        self.dispatch._active_history = True
+
+    @util.memoized_property
+    def _supports_population(self):
+        return self.impl.supports_population
+
+    @property
+    def _impl_uses_objects(self):
+        return self.impl.uses_objects
+
+    def get_history(self, instance, passive=PASSIVE_OFF):
+        return self.impl.get_history(
+            instance_state(instance), instance_dict(instance), passive
+        )
+
+    def __selectable__(self):
+        # TODO: conditionally attach this method based on clause_element ?
+        return self
+
+    @util.memoized_property
+    def info(self):
+        """Return the 'info' dictionary for the underlying SQL element.
+
+        The behavior here is as follows:
+
+        * If the attribute is a column-mapped property, i.e.
+          :class:`.ColumnProperty`, which is mapped directly
+          to a schema-level :class:`.Column` object, this attribute
+          will return the :attr:`.SchemaItem.info` dictionary associated
+          with the core-level :class:`.Column` object.
+
+        * If the attribute is a :class:`.ColumnProperty` but is mapped to
+          any other kind of SQL expression other than a :class:`.Column`,
+          the attribute will refer to the :attr:`.MapperProperty.info`
+          dictionary associated directly with the :class:`.ColumnProperty`,
+          assuming the SQL expression itself does not have its own ``.info``
+          attribute (which should be the case, unless a user-defined SQL
+          construct has defined one).
+
+        * If the attribute refers to any other kind of
+          :class:`.MapperProperty`, including :class:`.RelationshipProperty`,
+          the attribute will refer to the :attr:`.MapperProperty.info`
+          dictionary associated with that :class:`.MapperProperty`.
+
+        * To access the :attr:`.MapperProperty.info` dictionary of the
+          :class:`.MapperProperty` unconditionally, including for a
+          :class:`.ColumnProperty` that's associated directly with a
+          :class:`.schema.Column`, the attribute can be referred to using
+          :attr:`.QueryableAttribute.property` attribute, as
+          ``MyClass.someattribute.property.info``.
+
+        .. seealso::
+
+            :attr:`.SchemaItem.info`
+
+            :attr:`.MapperProperty.info`
+
+        """
+        return self.comparator.info
+
+    @util.memoized_property
+    def parent(self):
+        """Return an inspection instance representing the parent.
+
+        This will be either an instance of :class:`.Mapper`
+        or :class:`.AliasedInsp`, depending upon the nature
+        of the parent entity which this attribute is associated
+        with.
+
+        """
+        return inspection.inspect(self._parententity)
+
+    @property
+    def expression(self):
+        return self.comparator.__clause_element__()
+
+    def __clause_element__(self):
+        return self.comparator.__clause_element__()
+
+    def _query_clause_element(self):
+        """like __clause_element__(), but called specifically
+        by :class:`.Query` to allow special behavior."""
+
+        return self.comparator._query_clause_element()
+
+    def _bulk_update_tuples(self, value):
+        """Return setter tuples for a bulk UPDATE."""
+
+        return self.comparator._bulk_update_tuples(value)
+
+    def adapt_to_entity(self, adapt_to_entity):
+        assert not self._of_type
+        return self.__class__(
+            adapt_to_entity.entity,
+            self.key,
+            impl=self.impl,
+            comparator=self.comparator.adapt_to_entity(adapt_to_entity),
+            parententity=adapt_to_entity,
+        )
+
+    def of_type(self, cls):
+        return QueryableAttribute(
+            self.class_,
+            self.key,
+            self.impl,
+            self.comparator.of_type(cls),
+            self._parententity,
+            of_type=cls,
+        )
+
+    def label(self, name):
+        return self._query_clause_element().label(name)
+
+    def operate(self, op, *other, **kwargs):
+        return op(self.comparator, *other, **kwargs)
+
+    def reverse_operate(self, op, other, **kwargs):
+        return op(other, self.comparator, **kwargs)
+
+    def hasparent(self, state, optimistic=False):
+        return self.impl.hasparent(state, optimistic=optimistic) is not False
+
+    def __getattr__(self, key):
+        try:
+            return getattr(self.comparator, key)
+        except AttributeError as err:
+            util.raise_(
+                AttributeError(
+                    "Neither %r object nor %r object associated with %s "
+                    "has an attribute %r"
+                    % (
+                        type(self).__name__,
+                        type(self.comparator).__name__,
+                        self,
+                        key,
+                    )
+                ),
+                replace_context=err,
+            )
+
+    def __str__(self):
+        return "%s.%s" % (self.class_.__name__, self.key)
+
+    @util.memoized_property
+    def property(self):
+        """Return the :class:`.MapperProperty` associated with this
+        :class:`.QueryableAttribute`.
+
+
+        Return values here will commonly be instances of
+        :class:`.ColumnProperty` or :class:`.RelationshipProperty`.
+
+
+        """
+        return self.comparator.property
+
+
+class InstrumentedAttribute(QueryableAttribute):
+    """Class bound instrumented attribute which adds basic
+    :term:`descriptor` methods.
+
+    See :class:`.QueryableAttribute` for a description of most features.
+
+
+    """
+
+    def __set__(self, instance, value):
+        self.impl.set(
+            instance_state(instance), instance_dict(instance), value, None
+        )
+
+    def __delete__(self, instance):
+        self.impl.delete(instance_state(instance), instance_dict(instance))
+
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+
+        dict_ = instance_dict(instance)
+        if self._supports_population and self.key in dict_:
+            return dict_[self.key]
+        else:
+            return self.impl.get(instance_state(instance), dict_)
+
+
+def create_proxied_attribute(descriptor):
+    """Create an QueryableAttribute / user descriptor hybrid.
+
+    Returns a new QueryableAttribute type that delegates descriptor
+    behavior and getattr() to the given descriptor.
+    """
+
+    # TODO: can move this to descriptor_props if the need for this
+    # function is removed from ext/hybrid.py
+
+    class Proxy(QueryableAttribute):
+        """Presents the :class:`.QueryableAttribute` interface as a
+        proxy on top of a Python descriptor / :class:`.PropComparator`
+        combination.
+
+        """
+
+        def __init__(
+            self,
+            class_,
+            key,
+            descriptor,
+            comparator,
+            adapt_to_entity=None,
+            doc=None,
+            original_property=None,
+        ):
+            self.class_ = class_
+            self.key = key
+            self.descriptor = descriptor
+            self.original_property = original_property
+            self._comparator = comparator
+            self._adapt_to_entity = adapt_to_entity
+            self.__doc__ = doc
+
+        _is_internal_proxy = True
+
+        @property
+        def _impl_uses_objects(self):
+            return (
+                self.original_property is not None
+                and getattr(self.class_, self.key).impl.uses_objects
+            )
+
+        @property
+        def property(self):
+            return self.comparator.property
+
+        @util.memoized_property
+        def comparator(self):
+            if util.callable(self._comparator):
+                self._comparator = self._comparator()
+            if self._adapt_to_entity:
+                self._comparator = self._comparator.adapt_to_entity(
+                    self._adapt_to_entity
+                )
+            return self._comparator
+
+        def adapt_to_entity(self, adapt_to_entity):
+            return self.__class__(
+                adapt_to_entity.entity,
+                self.key,
+                self.descriptor,
+                self._comparator,
+                adapt_to_entity,
+            )
+
+        def __get__(self, instance, owner):
+            retval = self.descriptor.__get__(instance, owner)
+            # detect if this is a plain Python @property, which just returns
+            # itself for class level access.  If so, then return us.
+            # Otherwise, return the object returned by the descriptor.
+            if retval is self.descriptor and instance is None:
+                return self
+            else:
+                return retval
+
+        def __str__(self):
+            return "%s.%s" % (self.class_.__name__, self.key)
+
+        def __getattr__(self, attribute):
+            """Delegate __getattr__ to the original descriptor and/or
+            comparator."""
+            try:
+                return getattr(descriptor, attribute)
+            except AttributeError as err:
+                if attribute == "comparator":
+                    util.raise_(
+                        AttributeError("comparator"), replace_context=err
+                    )
+                try:
+                    # comparator itself might be unreachable
+                    comparator = self.comparator
+                except AttributeError as err2:
+                    util.raise_(
+                        AttributeError(
+                            "Neither %r object nor unconfigured comparator "
+                            "object associated with %s has an attribute %r"
+                            % (type(descriptor).__name__, self, attribute)
+                        ),
+                        replace_context=err2,
+                    )
+                else:
+                    try:
+                        return getattr(comparator, attribute)
+                    except AttributeError as err3:
+                        util.raise_(
+                            AttributeError(
+                                "Neither %r object nor %r object "
+                                "associated with %s has an attribute %r"
+                                % (
+                                    type(descriptor).__name__,
+                                    type(comparator).__name__,
+                                    self,
+                                    attribute,
+                                )
+                            ),
+                            replace_context=err3,
+                        )
+
+    Proxy.__name__ = type(descriptor).__name__ + "Proxy"
+
+    util.monkeypatch_proxied_specials(
+        Proxy, type(descriptor), name="descriptor", from_instance=descriptor
+    )
+    return Proxy
+
+
+OP_REMOVE = util.symbol("REMOVE")
+OP_APPEND = util.symbol("APPEND")
+OP_REPLACE = util.symbol("REPLACE")
+OP_BULK_REPLACE = util.symbol("BULK_REPLACE")
+OP_MODIFIED = util.symbol("MODIFIED")
+
+
+class Event(object):
+    """A token propagated throughout the course of a chain of attribute
+    events.
+
+    Serves as an indicator of the source of the event and also provides
+    a means of controlling propagation across a chain of attribute
+    operations.
+
+    The :class:`.Event` object is sent as the ``initiator`` argument
+    when dealing with events such as :meth:`.AttributeEvents.append`,
+    :meth:`.AttributeEvents.set`,
+    and :meth:`.AttributeEvents.remove`.
+
+    The :class:`.Event` object is currently interpreted by the backref
+    event handlers, and is used to control the propagation of operations
+    across two mutually-dependent attributes.
+
+    .. versionadded:: 0.9.0
+
+    :var impl: The :class:`.AttributeImpl` which is the current event
+     initiator.
+
+    :var op: The symbol :attr:`.OP_APPEND`, :attr:`.OP_REMOVE`,
+     :attr:`.OP_REPLACE`, or :attr:`.OP_BULK_REPLACE`, indicating the
+     source operation.
+
+    """
+
+    __slots__ = "impl", "op", "parent_token"
+
+    def __init__(self, attribute_impl, op):
+        self.impl = attribute_impl
+        self.op = op
+        self.parent_token = self.impl.parent_token
+
+    def __eq__(self, other):
+        return (
+            isinstance(other, Event)
+            and other.impl is self.impl
+            and other.op == self.op
+        )
+
+    @property
+    def key(self):
+        return self.impl.key
+
+    def hasparent(self, state):
+        return self.impl.hasparent(state)
+
+
+class AttributeImpl(object):
+    """internal implementation for instrumented attributes."""
+
+    def __init__(
+        self,
+        class_,
+        key,
+        callable_,
+        dispatch,
+        trackparent=False,
+        extension=None,
+        compare_function=None,
+        active_history=False,
+        parent_token=None,
+        expire_missing=True,
+        send_modified_events=True,
+        accepts_scalar_loader=None,
+        **kwargs
+    ):
+        r"""Construct an AttributeImpl.
+
+        :param \class_: associated class
+
+        :param key: string name of the attribute
+
+        :param \callable_:
+          optional function which generates a callable based on a parent
+          instance, which produces the "default" values for a scalar or
+          collection attribute when it's first accessed, if not present
+          already.
+
+        :param trackparent:
+          if True, attempt to track if an instance has a parent attached
+          to it via this attribute.
+
+        :param extension:
+          a single or list of AttributeExtension object(s) which will
+          receive set/delete/append/remove/etc. events.
+          The event package is now used.
+
+          .. deprecated::  1.3
+
+            The :paramref:`.AttributeImpl.extension` parameter is deprecated
+            and will be removed in a future release, corresponding to the
+            "extension" parameter on the :class:`.MapperProprty` classes
+            like :func:`.column_property` and :func:`.relationship`  The
+            events system is now used.
+
+        :param compare_function:
+          a function that compares two values which are normally
+          assignable to this attribute.
+
+        :param active_history:
+          indicates that get_history() should always return the "old" value,
+          even if it means executing a lazy callable upon attribute change.
+
+        :param parent_token:
+          Usually references the MapperProperty, used as a key for
+          the hasparent() function to identify an "owning" attribute.
+          Allows multiple AttributeImpls to all match a single
+          owner attribute.
+
+        :param expire_missing:
+          if False, don't add an "expiry" callable to this attribute
+          during state.expire_attributes(None), if no value is present
+          for this key.
+
+        :param send_modified_events:
+          if False, the InstanceState._modified_event method will have no
+          effect; this means the attribute will never show up as changed in a
+          history entry.
+
+        """
+        self.class_ = class_
+        self.key = key
+        self.callable_ = callable_
+        self.dispatch = dispatch
+        self.trackparent = trackparent
+        self.parent_token = parent_token or self
+        self.send_modified_events = send_modified_events
+        if compare_function is None:
+            self.is_equal = operator.eq
+        else:
+            self.is_equal = compare_function
+
+        if accepts_scalar_loader is not None:
+            self.accepts_scalar_loader = accepts_scalar_loader
+        else:
+            self.accepts_scalar_loader = self.default_accepts_scalar_loader
+
+        # TODO: pass in the manager here
+        # instead of doing a lookup
+        attr = manager_of_class(class_)[key]
+
+        for ext in util.to_list(extension or []):
+            ext._adapt_listener(attr, ext)
+
+        if active_history:
+            self.dispatch._active_history = True
+
+        self.expire_missing = expire_missing
+        self._modified_token = Event(self, OP_MODIFIED)
+
+    __slots__ = (
+        "class_",
+        "key",
+        "callable_",
+        "dispatch",
+        "trackparent",
+        "parent_token",
+        "send_modified_events",
+        "is_equal",
+        "expire_missing",
+        "_modified_token",
+        "accepts_scalar_loader",
+    )
+
+    def __str__(self):
+        return "%s.%s" % (self.class_.__name__, self.key)
+
+    def _get_active_history(self):
+        """Backwards compat for impl.active_history"""
+
+        return self.dispatch._active_history
+
+    def _set_active_history(self, value):
+        self.dispatch._active_history = value
+
+    active_history = property(_get_active_history, _set_active_history)
+
+    def hasparent(self, state, optimistic=False):
+        """Return the boolean value of a `hasparent` flag attached to
+        the given state.
+
+        The `optimistic` flag determines what the default return value
+        should be if no `hasparent` flag can be located.
+
+        As this function is used to determine if an instance is an
+        *orphan*, instances that were loaded from storage should be
+        assumed to not be orphans, until a True/False value for this
+        flag is set.
+
+        An instance attribute that is loaded by a callable function
+        will also not have a `hasparent` flag.
+
+        """
+        msg = "This AttributeImpl is not configured to track parents."
+        assert self.trackparent, msg
+
+        return (
+            state.parents.get(id(self.parent_token), optimistic) is not False
+        )
+
+    def sethasparent(self, state, parent_state, value):
+        """Set a boolean flag on the given item corresponding to
+        whether or not it is attached to a parent object via the
+        attribute represented by this ``InstrumentedAttribute``.
+
+        """
+        msg = "This AttributeImpl is not configured to track parents."
+        assert self.trackparent, msg
+
+        id_ = id(self.parent_token)
+        if value:
+            state.parents[id_] = parent_state
+        else:
+            if id_ in state.parents:
+                last_parent = state.parents[id_]
+
+                if (
+                    last_parent is not False
+                    and last_parent.key != parent_state.key
+                ):
+
+                    if last_parent.obj() is None:
+                        raise orm_exc.StaleDataError(
+                            "Removing state %s from parent "
+                            "state %s along attribute '%s', "
+                            "but the parent record "
+                            "has gone stale, can't be sure this "
+                            "is the most recent parent."
+                            % (
+                                state_str(state),
+                                state_str(parent_state),
+                                self.key,
+                            )
+                        )
+
+                    return
+
+            state.parents[id_] = False
+
+    def get_history(self, state, dict_, passive=PASSIVE_OFF):
+        raise NotImplementedError()
+
+    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
+        """Return a list of tuples of (state, obj)
+        for all objects in this attribute's current state
+        + history.
+
+        Only applies to object-based attributes.
+
+        This is an inlining of existing functionality
+        which roughly corresponds to:
+
+            get_state_history(
+                        state,
+                        key,
+                        passive=PASSIVE_NO_INITIALIZE).sum()
+
+        """
+        raise NotImplementedError()
+
+    def initialize(self, state, dict_):
+        """Initialize the given state's attribute with an empty value."""
+
+        value = None
+        for fn in self.dispatch.init_scalar:
+            ret = fn(state, value, dict_)
+            if ret is not ATTR_EMPTY:
+                value = ret
+
+        return value
+
+    def get(self, state, dict_, passive=PASSIVE_OFF):
+        """Retrieve a value from the given object.
+        If a callable is assembled on this object's attribute, and
+        passive is False, the callable will be executed and the
+        resulting value will be set as the new value for this attribute.
+        """
+        if self.key in dict_:
+            return dict_[self.key]
+        else:
+            # if history present, don't load
+            key = self.key
+            if (
+                key not in state.committed_state
+                or state.committed_state[key] is NEVER_SET
+            ):
+                if not passive & CALLABLES_OK:
+                    return PASSIVE_NO_RESULT
+
+                if key in state.expired_attributes:
+                    value = state._load_expired(state, passive)
+                elif key in state.callables:
+                    callable_ = state.callables[key]
+                    value = callable_(state, passive)
+                elif self.callable_:
+                    value = self.callable_(state, passive)
+                else:
+                    value = ATTR_EMPTY
+
+                if value is PASSIVE_NO_RESULT or value is NEVER_SET:
+                    return value
+                elif value is ATTR_WAS_SET:
+                    try:
+                        return dict_[key]
+                    except KeyError as err:
+                        # TODO: no test coverage here.
+                        util.raise_(
+                            KeyError(
+                                "Deferred loader for attribute "
+                                "%r failed to populate "
+                                "correctly" % key
+                            ),
+                            replace_context=err,
+                        )
+                elif value is not ATTR_EMPTY:
+                    return self.set_committed_value(state, dict_, value)
+
+            if not passive & INIT_OK:
+                return NEVER_SET
+            else:
+                # Return a new, empty value
+                return self.initialize(state, dict_)
+
+    def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
+        self.set(state, dict_, value, initiator, passive=passive)
+
+    def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
+        self.set(
+            state, dict_, None, initiator, passive=passive, check_old=value
+        )
+
+    def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
+        self.set(
+            state,
+            dict_,
+            None,
+            initiator,
+            passive=passive,
+            check_old=value,
+            pop=True,
+        )
+
+    def set(
+        self,
+        state,
+        dict_,
+        value,
+        initiator,
+        passive=PASSIVE_OFF,
+        check_old=None,
+        pop=False,
+    ):
+        raise NotImplementedError()
+
+    def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
+        """return the unchanged value of this attribute"""
+
+        if self.key in state.committed_state:
+            value = state.committed_state[self.key]
+            if value in (NO_VALUE, NEVER_SET):
+                return None
+            else:
+                return value
+        else:
+            return self.get(state, dict_, passive=passive)
+
+    def set_committed_value(self, state, dict_, value):
+        """set an attribute value on the given instance and 'commit' it."""
+
+        dict_[self.key] = value
+        state._commit(dict_, [self.key])
+        return value
+
+
+class ScalarAttributeImpl(AttributeImpl):
+    """represents a scalar value-holding InstrumentedAttribute."""
+
+    default_accepts_scalar_loader = True
+    uses_objects = False
+    supports_population = True
+    collection = False
+    dynamic = False
+
+    __slots__ = "_replace_token", "_append_token", "_remove_token"
+
+    def __init__(self, *arg, **kw):
+        super(ScalarAttributeImpl, self).__init__(*arg, **kw)
+        self._replace_token = self._append_token = Event(self, OP_REPLACE)
+        self._remove_token = Event(self, OP_REMOVE)
+
+    def delete(self, state, dict_):
+        if self.dispatch._active_history:
+            old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
+        else:
+            old = dict_.get(self.key, NO_VALUE)
+
+        if self.dispatch.remove:
+            self.fire_remove_event(state, dict_, old, self._remove_token)
+        state._modified_event(dict_, self, old)
+
+        existing = dict_.pop(self.key, NO_VALUE)
+        if (
+            existing is NO_VALUE
+            and old is NO_VALUE
+            and not state.expired
+            and self.key not in state.expired_attributes
+        ):
+            raise AttributeError("%s object does not have a value" % self)
+
+    def get_history(self, state, dict_, passive=PASSIVE_OFF):
+        if self.key in dict_:
+            return History.from_scalar_attribute(self, state, dict_[self.key])
+        else:
+            if passive & INIT_OK:
+                passive ^= INIT_OK
+            current = self.get(state, dict_, passive=passive)
+            if current is PASSIVE_NO_RESULT:
+                return HISTORY_BLANK
+            else:
+                return History.from_scalar_attribute(self, state, current)
+
+    def set(
+        self,
+        state,
+        dict_,
+        value,
+        initiator,
+        passive=PASSIVE_OFF,
+        check_old=None,
+        pop=False,
+    ):
+        if self.dispatch._active_history:
+            old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
+        else:
+            old = dict_.get(self.key, NO_VALUE)
+
+        if self.dispatch.set:
+            value = self.fire_replace_event(
+                state, dict_, value, old, initiator
+            )
+        state._modified_event(dict_, self, old)
+        dict_[self.key] = value
+
+    def fire_replace_event(self, state, dict_, value, previous, initiator):
+        for fn in self.dispatch.set:
+            value = fn(
+                state, value, previous, initiator or self._replace_token
+            )
+        return value
+
+    def fire_remove_event(self, state, dict_, value, initiator):
+        for fn in self.dispatch.remove:
+            fn(state, value, initiator or self._remove_token)
+
+    @property
+    def type(self):
+        self.property.columns[0].type
+
+
+class ScalarObjectAttributeImpl(ScalarAttributeImpl):
+    """represents a scalar-holding InstrumentedAttribute,
+       where the target object is also instrumented.
+
+       Adds events to delete/set operations.
+
+    """
+
+    default_accepts_scalar_loader = False
+    uses_objects = True
+    supports_population = True
+    collection = False
+
+    __slots__ = ()
+
+    def delete(self, state, dict_):
+        if self.dispatch._active_history:
+            old = self.get(
+                state,
+                dict_,
+                passive=PASSIVE_ONLY_PERSISTENT
+                | NO_AUTOFLUSH
+                | LOAD_AGAINST_COMMITTED,
+            )
+        else:
+            old = self.get(
+                state,
+                dict_,
+                passive=PASSIVE_NO_FETCH ^ INIT_OK
+                | LOAD_AGAINST_COMMITTED
+                | NO_RAISE,
+            )
+
+        self.fire_remove_event(state, dict_, old, self._remove_token)
+
+        existing = dict_.pop(self.key, NO_VALUE)
+
+        # if the attribute is expired, we currently have no way to tell
+        # that an object-attribute was expired vs. not loaded.   So
+        # for this test, we look to see if the object has a DB identity.
+        if (
+            existing is NO_VALUE
+            and old is not PASSIVE_NO_RESULT
+            and state.key is None
+        ):
+            raise AttributeError("%s object does not have a value" % self)
+
+    def get_history(self, state, dict_, passive=PASSIVE_OFF):
+        if self.key in dict_:
+            return History.from_object_attribute(self, state, dict_[self.key])
+        else:
+            if passive & INIT_OK:
+                passive ^= INIT_OK
+            current = self.get(state, dict_, passive=passive)
+            if current is PASSIVE_NO_RESULT:
+                return HISTORY_BLANK
+            else:
+                return History.from_object_attribute(self, state, current)
+
+    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
+        if self.key in dict_:
+            current = dict_[self.key]
+        elif passive & CALLABLES_OK:
+            current = self.get(state, dict_, passive=passive)
+        else:
+            return []
+
+        # can't use __hash__(), can't use __eq__() here
+        if (
+            current is not None
+            and current is not PASSIVE_NO_RESULT
+            and current is not NEVER_SET
+        ):
+            ret = [(instance_state(current), current)]
+        else:
+            ret = [(None, None)]
+
+        if self.key in state.committed_state:
+            original = state.committed_state[self.key]
+            if (
+                original is not None
+                and original is not PASSIVE_NO_RESULT
+                and original is not NEVER_SET
+                and original is not current
+            ):
+
+                ret.append((instance_state(original), original))
+        return ret
+
+    def set(
+        self,
+        state,
+        dict_,
+        value,
+        initiator,
+        passive=PASSIVE_OFF,
+        check_old=None,
+        pop=False,
+    ):
+        """Set a value on the given InstanceState.
+
+        """
+        if self.dispatch._active_history:
+            old = self.get(
+                state,
+                dict_,
+                passive=PASSIVE_ONLY_PERSISTENT
+                | NO_AUTOFLUSH
+                | LOAD_AGAINST_COMMITTED,
+            )
+        else:
+            old = self.get(
+                state,
+                dict_,
+                passive=PASSIVE_NO_FETCH ^ INIT_OK
+                | LOAD_AGAINST_COMMITTED
+                | NO_RAISE,
+            )
+
+        if (
+            check_old is not None
+            and old is not PASSIVE_NO_RESULT
+            and check_old is not old
+        ):
+            if pop:
+                return
+            else:
+                raise ValueError(
+                    "Object %s not associated with %s on attribute '%s'"
+                    % (instance_str(check_old), state_str(state), self.key)
+                )
+
+        value = self.fire_replace_event(state, dict_, value, old, initiator)
+        dict_[self.key] = value
+
+    def fire_remove_event(self, state, dict_, value, initiator):
+        if self.trackparent and value is not None:
+            self.sethasparent(instance_state(value), state, False)
+
+        for fn in self.dispatch.remove:
+            fn(state, value, initiator or self._remove_token)
+
+        state._modified_event(dict_, self, value)
+
+    def fire_replace_event(self, state, dict_, value, previous, initiator):
+        if self.trackparent:
+            if previous is not value and previous not in (
+                None,
+                PASSIVE_NO_RESULT,
+                NEVER_SET,
+            ):
+                self.sethasparent(instance_state(previous), state, False)
+
+        for fn in self.dispatch.set:
+            value = fn(
+                state, value, previous, initiator or self._replace_token
+            )
+
+        state._modified_event(dict_, self, previous)
+
+        if self.trackparent:
+            if value is not None:
+                self.sethasparent(instance_state(value), state, True)
+
+        return value
+
+
+class CollectionAttributeImpl(AttributeImpl):
+    """A collection-holding attribute that instruments changes in membership.
+
+    Only handles collections of instrumented objects.
+
+    InstrumentedCollectionAttribute holds an arbitrary, user-specified
+    container object (defaulting to a list) and brokers access to the
+    CollectionAdapter, a "view" onto that object that presents consistent bag
+    semantics to the orm layer independent of the user data implementation.
+
+    """
+
+    default_accepts_scalar_loader = False
+    uses_objects = True
+    supports_population = True
+    collection = True
+    dynamic = False
+
+    __slots__ = (
+        "copy",
+        "collection_factory",
+        "_append_token",
+        "_remove_token",
+        "_bulk_replace_token",
+        "_duck_typed_as",
+    )
+
+    def __init__(
+        self,
+        class_,
+        key,
+        callable_,
+        dispatch,
+        typecallable=None,
+        trackparent=False,
+        extension=None,
+        copy_function=None,
+        compare_function=None,
+        **kwargs
+    ):
+        super(CollectionAttributeImpl, self).__init__(
+            class_,
+            key,
+            callable_,
+            dispatch,
+            trackparent=trackparent,
+            extension=extension,
+            compare_function=compare_function,
+            **kwargs
+        )
+
+        if copy_function is None:
+            copy_function = self.__copy
+        self.copy = copy_function
+        self.collection_factory = typecallable
+        self._append_token = Event(self, OP_APPEND)
+        self._remove_token = Event(self, OP_REMOVE)
+        self._bulk_replace_token = Event(self, OP_BULK_REPLACE)
+        self._duck_typed_as = util.duck_type_collection(
+            self.collection_factory()
+        )
+
+        if getattr(self.collection_factory, "_sa_linker", None):
+
+            @event.listens_for(self, "init_collection")
+            def link(target, collection, collection_adapter):
+                collection._sa_linker(collection_adapter)
+
+            @event.listens_for(self, "dispose_collection")
+            def unlink(target, collection, collection_adapter):
+                collection._sa_linker(None)
+
+    def __copy(self, item):
+        return [y for y in collections.collection_adapter(item)]
+
+    def get_history(self, state, dict_, passive=PASSIVE_OFF):
+        current = self.get(state, dict_, passive=passive)
+        if current is PASSIVE_NO_RESULT:
+            return HISTORY_BLANK
+        else:
+            return History.from_collection(self, state, current)
+
+    def get_all_pending(self, state, dict_, passive=PASSIVE_NO_INITIALIZE):
+        # NOTE: passive is ignored here at the moment
+
+        if self.key not in dict_:
+            return []
+
+        current = dict_[self.key]
+        current = getattr(current, "_sa_adapter")
+
+        if self.key in state.committed_state:
+            original = state.committed_state[self.key]
+            if original not in (NO_VALUE, NEVER_SET):
+                current_states = [
+                    ((c is not None) and instance_state(c) or None, c)
+                    for c in current
+                ]
+                original_states = [
+                    ((c is not None) and instance_state(c) or None, c)
+                    for c in original
+                ]
+
+                current_set = dict(current_states)
+                original_set = dict(original_states)
+
+                return (
+                    [
+                        (s, o)
+                        for s, o in current_states
+                        if s not in original_set
+                    ]
+                    + [(s, o) for s, o in current_states if s in original_set]
+                    + [
+                        (s, o)
+                        for s, o in original_states
+                        if s not in current_set
+                    ]
+                )
+
+        return [(instance_state(o), o) for o in current]
+
+    def fire_append_event(self, state, dict_, value, initiator):
+        for fn in self.dispatch.append:
+            value = fn(state, value, initiator or self._append_token)
+
+        state._modified_event(dict_, self, NEVER_SET, True)
+
+        if self.trackparent and value is not None:
+            self.sethasparent(instance_state(value), state, True)
+
+        return value
+
+    def fire_pre_remove_event(self, state, dict_, initiator):
+        """A special event used for pop() operations.
+
+        The "remove" event needs to have the item to be removed passed to
+        it, which in the case of pop from a set, we don't have a way to access
+        the item before the operation.   the event is used for all pop()
+        operations (even though set.pop is the one where it is really needed).
+
+        """
+        state._modified_event(dict_, self, NEVER_SET, True)
+
+    def fire_remove_event(self, state, dict_, value, initiator):
+        if self.trackparent and value is not None:
+            self.sethasparent(instance_state(value), state, False)
+
+        for fn in self.dispatch.remove:
+            fn(state, value, initiator or self._remove_token)
+
+        state._modified_event(dict_, self, NEVER_SET, True)
+
+    def delete(self, state, dict_):
+        if self.key not in dict_:
+            return
+
+        state._modified_event(dict_, self, NEVER_SET, True)
+
+        collection = self.get_collection(state, state.dict)
+        collection.clear_with_event()
+
+        # key is always present because we checked above.  e.g.
+        # del is a no-op if collection not present.
+        del dict_[self.key]
+
+    def initialize(self, state, dict_):
+        """Initialize this attribute with an empty collection."""
+
+        _, user_data = self._initialize_collection(state)
+        dict_[self.key] = user_data
+        return user_data
+
+    def _initialize_collection(self, state):
+
+        adapter, collection = state.manager.initialize_collection(
+            self.key, state, self.collection_factory
+        )
+
+        self.dispatch.init_collection(state, collection, adapter)
+
+        return adapter, collection
+
+    def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
+        collection = self.get_collection(state, dict_, passive=passive)
+        if collection is PASSIVE_NO_RESULT:
+            value = self.fire_append_event(state, dict_, value, initiator)
+            assert (
+                self.key not in dict_
+            ), "Collection was loaded during event handling."
+            state._get_pending_mutation(self.key).append(value)
+        else:
+            collection.append_with_event(value, initiator)
+
+    def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
+        collection = self.get_collection(state, state.dict, passive=passive)
+        if collection is PASSIVE_NO_RESULT:
+            self.fire_remove_event(state, dict_, value, initiator)
+            assert (
+                self.key not in dict_
+            ), "Collection was loaded during event handling."
+            state._get_pending_mutation(self.key).remove(value)
+        else:
+            collection.remove_with_event(value, initiator)
+
+    def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
+        try:
+            # TODO: better solution here would be to add
+            # a "popper" role to collections.py to complement
+            # "remover".
+            self.remove(state, dict_, value, initiator, passive=passive)
+        except (ValueError, KeyError, IndexError):
+            pass
+
+    def set(
+        self,
+        state,
+        dict_,
+        value,
+        initiator=None,
+        passive=PASSIVE_OFF,
+        pop=False,
+        _adapt=True,
+    ):
+        iterable = orig_iterable = value
+
+        # pulling a new collection first so that an adaptation exception does
+        # not trigger a lazy load of the old collection.
+        new_collection, user_data = self._initialize_collection(state)
+        if _adapt:
+            if new_collection._converter is not None:
+                iterable = new_collection._converter(iterable)
+            else:
+                setting_type = util.duck_type_collection(iterable)
+                receiving_type = self._duck_typed_as
+
+                if setting_type is not receiving_type:
+                    given = (
+                        iterable is None
+                        and "None"
+                        or iterable.__class__.__name__
+                    )
+                    wanted = self._duck_typed_as.__name__
+                    raise TypeError(
+                        "Incompatible collection type: %s is not %s-like"
+                        % (given, wanted)
+                    )
+
+                # If the object is an adapted collection, return the (iterable)
+                # adapter.
+                if hasattr(iterable, "_sa_iterator"):
+                    iterable = iterable._sa_iterator()
+                elif setting_type is dict:
+                    if util.py3k:
+                        iterable = iterable.values()
+                    else:
+                        iterable = getattr(
+                            iterable, "itervalues", iterable.values
+                        )()
+                else:
+                    iterable = iter(iterable)
+        new_values = list(iterable)
+
+        evt = self._bulk_replace_token
+
+        self.dispatch.bulk_replace(state, new_values, evt)
+
+        old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
+        if old is PASSIVE_NO_RESULT:
+            old = self.initialize(state, dict_)
+        elif old is orig_iterable:
+            # ignore re-assignment of the current collection, as happens
+            # implicitly with in-place operators (foo.collection |= other)
+            return
+
+        # place a copy of "old" in state.committed_state
+        state._modified_event(dict_, self, old, True)
+
+        old_collection = old._sa_adapter
+
+        dict_[self.key] = user_data
+
+        collections.bulk_replace(
+            new_values, old_collection, new_collection, initiator=evt
+        )
+
+        del old._sa_adapter
+        self.dispatch.dispose_collection(state, old, old_collection)
+
+    def _invalidate_collection(self, collection):
+        adapter = getattr(collection, "_sa_adapter")
+        adapter.invalidated = True
+
+    def set_committed_value(self, state, dict_, value):
+        """Set an attribute value on the given instance and 'commit' it."""
+
+        collection, user_data = self._initialize_collection(state)
+
+        if value:
+            collection.append_multiple_without_event(value)
+
+        state.dict[self.key] = user_data
+
+        state._commit(dict_, [self.key])
+
+        if self.key in state._pending_mutations:
+            # pending items exist.  issue a modified event,
+            # add/remove new items.
+            state._modified_event(dict_, self, user_data, True)
+
+            pending = state._pending_mutations.pop(self.key)
+            added = pending.added_items
+            removed = pending.deleted_items
+            for item in added:
+                collection.append_without_event(item)
+            for item in removed:
+                collection.remove_without_event(item)
+
+        return user_data
+
+    def get_collection(
+        self, state, dict_, user_data=None, passive=PASSIVE_OFF
+    ):
+        """Retrieve the CollectionAdapter associated with the given state.
+
+        Creates a new CollectionAdapter if one does not exist.
+
+        """
+        if user_data is None:
+            user_data = self.get(state, dict_, passive=passive)
+            if user_data is PASSIVE_NO_RESULT:
+                return user_data
+
+        return getattr(user_data, "_sa_adapter")
+
+
+def backref_listeners(attribute, key, uselist):
+    """Apply listeners to synchronize a two-way relationship."""
+
+    # use easily recognizable names for stack traces.
+
+    # in the sections marked "tokens to test for a recursive loop",
+    # this is somewhat brittle and very performance-sensitive logic
+    # that is specific to how we might arrive at each event.  a marker
+    # that can target us directly to arguments being invoked against
+    # the impl might be simpler, but could interfere with other systems.
+
+    parent_token = attribute.impl.parent_token
+    parent_impl = attribute.impl
+
+    def _acceptable_key_err(child_state, initiator, child_impl):
+        raise ValueError(
+            "Bidirectional attribute conflict detected: "
+            'Passing object %s to attribute "%s" '
+            'triggers a modify event on attribute "%s" '
+            'via the backref "%s".'
+            % (
+                state_str(child_state),
+                initiator.parent_token,
+                child_impl.parent_token,
+                attribute.impl.parent_token,
+            )
+        )
+
+    def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
+        if oldchild is child:
+            return child
+        if (
+            oldchild is not None
+            and oldchild is not PASSIVE_NO_RESULT
+            and oldchild is not NEVER_SET
+        ):
+            # With lazy=None, there's no guarantee that the full collection is
+            # present when updating via a backref.
+            old_state, old_dict = (
+                instance_state(oldchild),
+                instance_dict(oldchild),
+            )
+            impl = old_state.manager[key].impl
+
+            # tokens to test for a recursive loop.
+            if not impl.collection and not impl.dynamic:
+                check_recursive_token = impl._replace_token
+            else:
+                check_recursive_token = impl._remove_token
+
+            if initiator is not check_recursive_token:
+                impl.pop(
+                    old_state,
+                    old_dict,
+                    state.obj(),
+                    parent_impl._append_token,
+                    passive=PASSIVE_NO_FETCH,
+                )
+
+        if child is not None:
+            child_state, child_dict = (
+                instance_state(child),
+                instance_dict(child),
+            )
+            child_impl = child_state.manager[key].impl
+
+            if (
+                initiator.parent_token is not parent_token
+                and initiator.parent_token is not child_impl.parent_token
+            ):
+                _acceptable_key_err(state, initiator, child_impl)
+
+            # tokens to test for a recursive loop.
+            check_append_token = child_impl._append_token
+            check_bulk_replace_token = (
+                child_impl._bulk_replace_token
+                if child_impl.collection
+                else None
+            )
+
+            if (
+                initiator is not check_append_token
+                and initiator is not check_bulk_replace_token
+            ):
+                child_impl.append(
+                    child_state,
+                    child_dict,
+                    state.obj(),
+                    initiator,
+                    passive=PASSIVE_NO_FETCH,
+                )
+        return child
+
+    def emit_backref_from_collection_append_event(state, child, initiator):
+        if child is None:
+            return
+
+        child_state, child_dict = instance_state(child), instance_dict(child)
+        child_impl = child_state.manager[key].impl
+
+        if (
+            initiator.parent_token is not parent_token
+            and initiator.parent_token is not child_impl.parent_token
+        ):
+            _acceptable_key_err(state, initiator, child_impl)
+
+        # tokens to test for a recursive loop.
+        check_append_token = child_impl._append_token
+        check_bulk_replace_token = (
+            child_impl._bulk_replace_token if child_impl.collection else None
+        )
+
+        if (
+            initiator is not check_append_token
+            and initiator is not check_bulk_replace_token
+        ):
+            child_impl.append(
+                child_state,
+                child_dict,
+                state.obj(),
+                initiator,
+                passive=PASSIVE_NO_FETCH,
+            )
+        return child
+
+    def emit_backref_from_collection_remove_event(state, child, initiator):
+        if (
+            child is not None
+            and child is not PASSIVE_NO_RESULT
+            and child is not NEVER_SET
+        ):
+            child_state, child_dict = (
+                instance_state(child),
+                instance_dict(child),
+            )
+            child_impl = child_state.manager[key].impl
+
+            # tokens to test for a recursive loop.
+            if not child_impl.collection and not child_impl.dynamic:
+                check_remove_token = child_impl._remove_token
+                check_replace_token = child_impl._replace_token
+                check_for_dupes_on_remove = uselist and not parent_impl.dynamic
+            else:
+                check_remove_token = child_impl._remove_token
+                check_replace_token = (
+                    child_impl._bulk_replace_token
+                    if child_impl.collection
+                    else None
+                )
+                check_for_dupes_on_remove = False
+
+            if (
+                initiator is not check_remove_token
+                and initiator is not check_replace_token
+            ):
+
+                if not check_for_dupes_on_remove or not util.has_dupes(
+                    # when this event is called, the item is usually
+                    # present in the list, except for a pop() operation.
+                    state.dict[parent_impl.key],
+                    child,
+                ):
+                    child_impl.pop(
+                        child_state,
+                        child_dict,
+                        state.obj(),
+                        initiator,
+                        passive=PASSIVE_NO_FETCH,
+                    )
+
+    if uselist:
+        event.listen(
+            attribute,
+            "append",
+            emit_backref_from_collection_append_event,
+            retval=True,
+            raw=True,
+        )
+    else:
+        event.listen(
+            attribute,
+            "set",
+            emit_backref_from_scalar_set_event,
+            retval=True,
+            raw=True,
+        )
+    # TODO: need coverage in test/orm/ of remove event
+    event.listen(
+        attribute,
+        "remove",
+        emit_backref_from_collection_remove_event,
+        retval=True,
+        raw=True,
+    )
+
+
+_NO_HISTORY = util.symbol("NO_HISTORY")
+_NO_STATE_SYMBOLS = frozenset(
+    [id(PASSIVE_NO_RESULT), id(NO_VALUE), id(NEVER_SET)]
+)
+
+History = util.namedtuple("History", ["added", "unchanged", "deleted"])
+
+
+class History(History):
+    """A 3-tuple of added, unchanged and deleted values,
+    representing the changes which have occurred on an instrumented
+    attribute.
+
+    The easiest way to get a :class:`.History` object for a particular
+    attribute on an object is to use the :func:`.inspect` function::
+
+        from sqlalchemy import inspect
+
+        hist = inspect(myobject).attrs.myattribute.history
+
+    Each tuple member is an iterable sequence:
+
+    * ``added`` - the collection of items added to the attribute (the first
+      tuple element).
+
+    * ``unchanged`` - the collection of items that have not changed on the
+      attribute (the second tuple element).
+
+    * ``deleted`` - the collection of items that have been removed from the
+      attribute (the third tuple element).
+
+    """
+
+    def __bool__(self):
+        return self != HISTORY_BLANK
+
+    __nonzero__ = __bool__
+
+    def empty(self):
+        """Return True if this :class:`.History` has no changes
+        and no existing, unchanged state.
+
+        """
+
+        return not bool((self.added or self.deleted) or self.unchanged)
+
+    def sum(self):
+        """Return a collection of added + unchanged + deleted."""
+
+        return (
+            (self.added or []) + (self.unchanged or []) + (self.deleted or [])
+        )
+
+    def non_deleted(self):
+        """Return a collection of added + unchanged."""
+
+        return (self.added or []) + (self.unchanged or [])
+
+    def non_added(self):
+        """Return a collection of unchanged + deleted."""
+
+        return (self.unchanged or []) + (self.deleted or [])
+
+    def has_changes(self):
+        """Return True if this :class:`.History` has changes."""
+
+        return bool(self.added or self.deleted)
+
+    def as_state(self):
+        return History(
+            [
+                (c is not None) and instance_state(c) or None
+                for c in self.added
+            ],
+            [
+                (c is not None) and instance_state(c) or None
+                for c in self.unchanged
+            ],
+            [
+                (c is not None) and instance_state(c) or None
+                for c in self.deleted
+            ],
+        )
+
+    @classmethod
+    def from_scalar_attribute(cls, attribute, state, current):
+        original = state.committed_state.get(attribute.key, _NO_HISTORY)
+
+        if original is _NO_HISTORY:
+            if current is NEVER_SET:
+                return cls((), (), ())
+            else:
+                return cls((), [current], ())
+        # don't let ClauseElement expressions here trip things up
+        elif attribute.is_equal(current, original) is True:
+            return cls((), [current], ())
+        else:
+            # current convention on native scalars is to not
+            # include information
+            # about missing previous value in "deleted", but
+            # we do include None, which helps in some primary
+            # key situations
+            if id(original) in _NO_STATE_SYMBOLS:
+                deleted = ()
+                # indicate a "del" operation occurred when we don't have
+                # the previous value as: ([None], (), ())
+                if id(current) in _NO_STATE_SYMBOLS:
+                    current = None
+            else:
+                deleted = [original]
+            if current is NEVER_SET:
+                return cls((), (), deleted)
+            else:
+                return cls([current], (), deleted)
+
+    @classmethod
+    def from_object_attribute(cls, attribute, state, current):
+        original = state.committed_state.get(attribute.key, _NO_HISTORY)
+
+        if original is _NO_HISTORY:
+            if current is NO_VALUE or current is NEVER_SET:
+                return cls((), (), ())
+            else:
+                return cls((), [current], ())
+        elif current is original and current is not NEVER_SET:
+            return cls((), [current], ())
+        else:
+            # current convention on related objects is to not
+            # include information
+            # about missing previous value in "deleted", and
+            # to also not include None - the dependency.py rules
+            # ignore the None in any case.
+            if id(original) in _NO_STATE_SYMBOLS or original is None:
+                deleted = ()
+                # indicate a "del" operation occurred when we don't have
+                # the previous value as: ([None], (), ())
+                if id(current) in _NO_STATE_SYMBOLS:
+                    current = None
+            else:
+                deleted = [original]
+            if current is NO_VALUE or current is NEVER_SET:
+                return cls((), (), deleted)
+            else:
+                return cls([current], (), deleted)
+
+    @classmethod
+    def from_collection(cls, attribute, state, current):
+        original = state.committed_state.get(attribute.key, _NO_HISTORY)
+
+        if current is NO_VALUE or current is NEVER_SET:
+            return cls((), (), ())
+
+        current = getattr(current, "_sa_adapter")
+        if original in (NO_VALUE, NEVER_SET):
+            return cls(list(current), (), ())
+        elif original is _NO_HISTORY:
+            return cls((), list(current), ())
+        else:
+
+            current_states = [
+                ((c is not None) and instance_state(c) or None, c)
+                for c in current
+            ]
+            original_states = [
+                ((c is not None) and instance_state(c) or None, c)
+                for c in original
+            ]
+
+            current_set = dict(current_states)
+            original_set = dict(original_states)
+
+            return cls(
+                [o for s, o in current_states if s not in original_set],
+                [o for s, o in current_states if s in original_set],
+                [o for s, o in original_states if s not in current_set],
+            )
+
+
+HISTORY_BLANK = History(None, None, None)
+
+
+def get_history(obj, key, passive=PASSIVE_OFF):
+    """Return a :class:`.History` record for the given object
+    and attribute key.
+
+    This is the **pre-flush** history for a given attribute, which is
+    reset each time the :class:`.Session` flushes changes to the
+    current database transaction.
+
+    .. note::
+
+        Prefer to use the :attr:`.AttributeState.history` and
+        :meth:`.AttributeState.load_history` accessors to retrieve the
+        :class:`.History` for instance attributes.
+
+
+    :param obj: an object whose class is instrumented by the
+      attributes package.
+
+    :param key: string attribute name.
+
+    :param passive: indicates loading behavior for the attribute
+       if the value is not already present.   This is a
+       bitflag attribute, which defaults to the symbol
+       :attr:`.PASSIVE_OFF` indicating all necessary SQL
+       should be emitted.
+
+    .. seealso::
+
+        :attr:`.AttributeState.history`
+
+        :meth:`.AttributeState.load_history` - retrieve history
+        using loader callables if the value is not locally present.
+
+    """
+    if passive is True:
+        util.warn_deprecated(
+            "Passing True for 'passive' is deprecated. "
+            "Use attributes.PASSIVE_NO_INITIALIZE"
+        )
+        passive = PASSIVE_NO_INITIALIZE
+    elif passive is False:
+        util.warn_deprecated(
+            "Passing False for 'passive' is "
+            "deprecated.  Use attributes.PASSIVE_OFF"
+        )
+        passive = PASSIVE_OFF
+
+    return get_state_history(instance_state(obj), key, passive)
+
+
+def get_state_history(state, key, passive=PASSIVE_OFF):
+    return state.get_history(key, passive)
+
+
+def has_parent(cls, obj, key, optimistic=False):
+    """TODO"""
+    manager = manager_of_class(cls)
+    state = instance_state(obj)
+    return manager.has_parent(state, key, optimistic)
+
+
+def register_attribute(class_, key, **kw):
+    comparator = kw.pop("comparator", None)
+    parententity = kw.pop("parententity", None)
+    doc = kw.pop("doc", None)
+    desc = register_descriptor(class_, key, comparator, parententity, doc=doc)
+    register_attribute_impl(class_, key, **kw)
+    return desc
+
+
+def register_attribute_impl(
+    class_,
+    key,
+    uselist=False,
+    callable_=None,
+    useobject=False,
+    impl_class=None,
+    backref=None,
+    **kw
+):
+
+    manager = manager_of_class(class_)
+    if uselist:
+        factory = kw.pop("typecallable", None)
+        typecallable = manager.instrument_collection_class(
+            key, factory or list
+        )
+    else:
+        typecallable = kw.pop("typecallable", None)
+
+    dispatch = manager[key].dispatch
+
+    if impl_class:
+        impl = impl_class(class_, key, typecallable, dispatch, **kw)
+    elif uselist:
+        impl = CollectionAttributeImpl(
+            class_, key, callable_, dispatch, typecallable=typecallable, **kw
+        )
+    elif useobject:
+        impl = ScalarObjectAttributeImpl(
+            class_, key, callable_, dispatch, **kw
+        )
+    else:
+        impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
+
+    manager[key].impl = impl
+
+    if backref:
+        backref_listeners(manager[key], backref, uselist)
+
+    manager.post_configure_attribute(key)
+    return manager[key]
+
+
+def register_descriptor(
+    class_, key, comparator=None, parententity=None, doc=None
+):
+    manager = manager_of_class(class_)
+
+    descriptor = InstrumentedAttribute(
+        class_, key, comparator=comparator, parententity=parententity
+    )
+
+    descriptor.__doc__ = doc
+
+    manager.instrument_attribute(key, descriptor)
+    return descriptor
+
+
+def unregister_attribute(class_, key):
+    manager_of_class(class_).uninstrument_attribute(key)
+
+
+def init_collection(obj, key):
+    """Initialize a collection attribute and return the collection adapter.
+
+    This function is used to provide direct access to collection internals
+    for a previously unloaded attribute.  e.g.::
+
+        collection_adapter = init_collection(someobject, 'elements')
+        for elem in values:
+            collection_adapter.append_without_event(elem)
+
+    For an easier way to do the above, see
+    :func:`~sqlalchemy.orm.attributes.set_committed_value`.
+
+    :param obj: a mapped object
+
+    :param key: string attribute name where the collection is located.
+
+    """
+    state = instance_state(obj)
+    dict_ = state.dict
+    return init_state_collection(state, dict_, key)
+
+
+def init_state_collection(state, dict_, key):
+    """Initialize a collection attribute and return the collection adapter."""
+
+    attr = state.manager[key].impl
+    user_data = attr.initialize(state, dict_)
+    return attr.get_collection(state, dict_, user_data)
+
+
+def set_committed_value(instance, key, value):
+    """Set the value of an attribute with no history events.
+
+    Cancels any previous history present.  The value should be
+    a scalar value for scalar-holding attributes, or
+    an iterable for any collection-holding attribute.
+
+    This is the same underlying method used when a lazy loader
+    fires off and loads additional data from the database.
+    In particular, this method can be used by application code
+    which has loaded additional attributes or collections through
+    separate queries, which can then be attached to an instance
+    as though it were part of its original loaded state.
+
+    """
+    state, dict_ = instance_state(instance), instance_dict(instance)
+    state.manager[key].impl.set_committed_value(state, dict_, value)
+
+
+def set_attribute(instance, key, value, initiator=None):
+    """Set the value of an attribute, firing history events.
+
+    This function may be used regardless of instrumentation
+    applied directly to the class, i.e. no descriptors are required.
+    Custom attribute management schemes will need to make usage
+    of this method to establish attribute state as understood
+    by SQLAlchemy.
+
+    :param instance: the object that will be modified
+
+    :param key: string name of the attribute
+
+    :param value: value to assign
+
+    :param initiator: an instance of :class:`.Event` that would have
+     been propagated from a previous event listener.  This argument
+     is used when the :func:`.set_attribute` function is being used within
+     an existing event listening function where an :class:`.Event` object
+     is being supplied; the object may be used to track the origin of the
+     chain of events.
+
+     .. versionadded:: 1.2.3
+
+    """
+    state, dict_ = instance_state(instance), instance_dict(instance)
+    state.manager[key].impl.set(state, dict_, value, initiator)
+
+
+def get_attribute(instance, key):
+    """Get the value of an attribute, firing any callables required.
+
+    This function may be used regardless of instrumentation
+    applied directly to the class, i.e. no descriptors are required.
+    Custom attribute management schemes will need to make usage
+    of this method to make usage of attribute state as understood
+    by SQLAlchemy.
+
+    """
+    state, dict_ = instance_state(instance), instance_dict(instance)
+    return state.manager[key].impl.get(state, dict_)
+
+
+def del_attribute(instance, key):
+    """Delete the value of an attribute, firing history events.
+
+    This function may be used regardless of instrumentation
+    applied directly to the class, i.e. no descriptors are required.
+    Custom attribute management schemes will need to make usage
+    of this method to establish attribute state as understood
+    by SQLAlchemy.
+
+    """
+    state, dict_ = instance_state(instance), instance_dict(instance)
+    state.manager[key].impl.delete(state, dict_)
+
+
+def flag_modified(instance, key):
+    """Mark an attribute on an instance as 'modified'.
+
+    This sets the 'modified' flag on the instance and
+    establishes an unconditional change event for the given attribute.
+    The attribute must have a value present, else an
+    :class:`.InvalidRequestError` is raised.
+
+    To mark an object "dirty" without referring to any specific attribute
+    so that it is considered within a flush, use the
+    :func:`.attributes.flag_dirty` call.
+
+    .. seealso::
+
+        :func:`.attributes.flag_dirty`
+
+    """
+    state, dict_ = instance_state(instance), instance_dict(instance)
+    impl = state.manager[key].impl
+    impl.dispatch.modified(state, impl._modified_token)
+    state._modified_event(dict_, impl, NO_VALUE, is_userland=True)
+
+
+def flag_dirty(instance):
+    """Mark an instance as 'dirty' without any specific attribute mentioned.
+
+    This is a special operation that will allow the object to travel through
+    the flush process for interception by events such as
+    :meth:`.SessionEvents.before_flush`.   Note that no SQL will be emitted in
+    the flush process for an object that has no changes, even if marked dirty
+    via this method.  However, a :meth:`.SessionEvents.before_flush` handler
+    will be able to see the object in the :attr:`.Session.dirty` collection and
+    may establish changes on it, which will then be included in the SQL
+    emitted.
+
+    .. versionadded:: 1.2
+
+    .. seealso::
+
+        :func:`.attributes.flag_modified`
+
+    """
+
+    state, dict_ = instance_state(instance), instance_dict(instance)
+    state._modified_event(dict_, None, NO_VALUE, is_userland=True)
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/wtforms/fields/core.html b/docs/_build/html/_modules/wtforms/fields/core.html new file mode 100644 index 00000000..c847f093 --- /dev/null +++ b/docs/_build/html/_modules/wtforms/fields/core.html @@ -0,0 +1,1117 @@ + + + + + + + + wtforms.fields.core — AIPscan 0.x documentation + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Source code for wtforms.fields.core

+from __future__ import unicode_literals
+
+import datetime
+import decimal
+import itertools
+
+from copy import copy
+
+from wtforms import widgets
+from wtforms.compat import text_type, izip
+from wtforms.i18n import DummyTranslations
+from wtforms.validators import StopValidation
+from wtforms.utils import unset_value
+
+
+__all__ = (
+    'BooleanField', 'DecimalField', 'DateField', 'DateTimeField', 'FieldList',
+    'FloatField', 'FormField', 'IntegerField', 'RadioField', 'SelectField',
+    'SelectMultipleField', 'StringField', 'TimeField',
+)
+
+
+class Field(object):
+    """
+    Field base class
+    """
+    errors = tuple()
+    process_errors = tuple()
+    raw_data = None
+    validators = tuple()
+    widget = None
+    _formfield = True
+    _translations = DummyTranslations()
+    do_not_call_in_templates = True  # Allow Django 1.4 traversal
+
+    def __new__(cls, *args, **kwargs):
+        if '_form' in kwargs and '_name' in kwargs:
+            return super(Field, cls).__new__(cls)
+        else:
+            return UnboundField(cls, *args, **kwargs)
+
+    def __init__(self, label=None, validators=None, filters=tuple(),
+                 description='', id=None, default=None, widget=None,
+                 render_kw=None, _form=None, _name=None, _prefix='',
+                 _translations=None, _meta=None):
+        """
+        Construct a new field.
+
+        :param label:
+            The label of the field.
+        :param validators:
+            A sequence of validators to call when `validate` is called.
+        :param filters:
+            A sequence of filters which are run on input data by `process`.
+        :param description:
+            A description for the field, typically used for help text.
+        :param id:
+            An id to use for the field. A reasonable default is set by the form,
+            and you shouldn't need to set this manually.
+        :param default:
+            The default value to assign to the field, if no form or object
+            input is provided. May be a callable.
+        :param widget:
+            If provided, overrides the widget used to render the field.
+        :param dict render_kw:
+            If provided, a dictionary which provides default keywords that
+            will be given to the widget at render time.
+        :param _form:
+            The form holding this field. It is passed by the form itself during
+            construction. You should never pass this value yourself.
+        :param _name:
+            The name of this field, passed by the enclosing form during its
+            construction. You should never pass this value yourself.
+        :param _prefix:
+            The prefix to prepend to the form name of this field, passed by
+            the enclosing form during construction.
+        :param _translations:
+            A translations object providing message translations. Usually
+            passed by the enclosing form during construction. See
+            :doc:`I18n docs <i18n>` for information on message translations.
+        :param _meta:
+            If provided, this is the 'meta' instance from the form. You usually
+            don't pass this yourself.
+
+        If `_form` and `_name` isn't provided, an :class:`UnboundField` will be
+        returned instead. Call its :func:`bind` method with a form instance and
+        a name to construct the field.
+        """
+        if _translations is not None:
+            self._translations = _translations
+
+        if _meta is not None:
+            self.meta = _meta
+        elif _form is not None:
+            self.meta = _form.meta
+        else:
+            raise TypeError("Must provide one of _form or _meta")
+
+        self.default = default
+        self.description = description
+        self.render_kw = render_kw
+        self.filters = filters
+        self.flags = Flags()
+        self.name = _prefix + _name
+        self.short_name = _name
+        self.type = type(self).__name__
+        self.validators = validators or list(self.validators)
+
+        self.id = id or self.name
+        self.label = Label(self.id, label if label is not None else self.gettext(_name.replace('_', ' ').title()))
+
+        if widget is not None:
+            self.widget = widget
+
+        for v in itertools.chain(self.validators, [self.widget]):
+            flags = getattr(v, 'field_flags', ())
+            for f in flags:
+                setattr(self.flags, f, True)
+
+    def __unicode__(self):
+        """
+        Returns a HTML representation of the field. For more powerful rendering,
+        see the `__call__` method.
+        """
+        return self()
+
+    def __str__(self):
+        """
+        Returns a HTML representation of the field. For more powerful rendering,
+        see the `__call__` method.
+        """
+        return self()
+
+    def __html__(self):
+        """
+        Returns a HTML representation of the field. For more powerful rendering,
+        see the :meth:`__call__` method.
+        """
+        return self()
+
+    def __call__(self, **kwargs):
+        """
+        Render this field as HTML, using keyword args as additional attributes.
+
+        This delegates rendering to
+        :meth:`meta.render_field <wtforms.meta.DefaultMeta.render_field>`
+        whose default behavior is to call the field's widget, passing any
+        keyword arguments from this call along to the widget.
+
+        In all of the WTForms HTML widgets, keyword arguments are turned to
+        HTML attributes, though in theory a widget is free to do anything it
+        wants with the supplied keyword arguments, and widgets don't have to
+        even do anything related to HTML.
+        """
+        return self.meta.render_field(self, kwargs)
+
+    def gettext(self, string):
+        """
+        Get a translation for the given message.
+
+        This proxies for the internal translations object.
+
+        :param string: A unicode string to be translated.
+        :return: A unicode string which is the translated output.
+        """
+        return self._translations.gettext(string)
+
+    def ngettext(self, singular, plural, n):
+        """
+        Get a translation for a message which can be pluralized.
+
+        :param str singular: The singular form of the message.
+        :param str plural: The plural form of the message.
+        :param int n: The number of elements this message is referring to
+        """
+        return self._translations.ngettext(singular, plural, n)
+
+    def validate(self, form, extra_validators=tuple()):
+        """
+        Validates the field and returns True or False. `self.errors` will
+        contain any errors raised during validation. This is usually only
+        called by `Form.validate`.
+
+        Subfields shouldn't override this, but rather override either
+        `pre_validate`, `post_validate` or both, depending on needs.
+
+        :param form: The form the field belongs to.
+        :param extra_validators: A sequence of extra validators to run.
+        """
+        self.errors = list(self.process_errors)
+        stop_validation = False
+
+        # Call pre_validate
+        try:
+            self.pre_validate(form)
+        except StopValidation as e:
+            if e.args and e.args[0]:
+                self.errors.append(e.args[0])
+            stop_validation = True
+        except ValueError as e:
+            self.errors.append(e.args[0])
+
+        # Run validators
+        if not stop_validation:
+            chain = itertools.chain(self.validators, extra_validators)
+            stop_validation = self._run_validation_chain(form, chain)
+
+        # Call post_validate
+        try:
+            self.post_validate(form, stop_validation)
+        except ValueError as e:
+            self.errors.append(e.args[0])
+
+        return len(self.errors) == 0
+
+    def _run_validation_chain(self, form, validators):
+        """
+        Run a validation chain, stopping if any validator raises StopValidation.
+
+        :param form: The Form instance this field belongs to.
+        :param validators: a sequence or iterable of validator callables.
+        :return: True if validation was stopped, False otherwise.
+        """
+        for validator in validators:
+            try:
+                validator(form, self)
+            except StopValidation as e:
+                if e.args and e.args[0]:
+                    self.errors.append(e.args[0])
+                return True
+            except ValueError as e:
+                self.errors.append(e.args[0])
+
+        return False
+
+    def pre_validate(self, form):
+        """
+        Override if you need field-level validation. Runs before any other
+        validators.
+
+        :param form: The form the field belongs to.
+        """
+        pass
+
+    def post_validate(self, form, validation_stopped):
+        """
+        Override if you need to run any field-level validation tasks after
+        normal validation. This shouldn't be needed in most cases.
+
+        :param form: The form the field belongs to.
+        :param validation_stopped:
+            `True` if any validator raised StopValidation.
+        """
+        pass
+
+    def process(self, formdata, data=unset_value):
+        """
+        Process incoming data, calling process_data, process_formdata as needed,
+        and run filters.
+
+        If `data` is not provided, process_data will be called on the field's
+        default.
+
+        Field subclasses usually won't override this, instead overriding the
+        process_formdata and process_data methods. Only override this for
+        special advanced processing, such as when a field encapsulates many
+        inputs.
+        """
+        self.process_errors = []
+        if data is unset_value:
+            try:
+                data = self.default()
+            except TypeError:
+                data = self.default
+
+        self.object_data = data
+
+        try:
+            self.process_data(data)
+        except ValueError as e:
+            self.process_errors.append(e.args[0])
+
+        if formdata is not None:
+            if self.name in formdata:
+                self.raw_data = formdata.getlist(self.name)
+            else:
+                self.raw_data = []
+
+            try:
+                self.process_formdata(self.raw_data)
+            except ValueError as e:
+                self.process_errors.append(e.args[0])
+
+        try:
+            for filter in self.filters:
+                self.data = filter(self.data)
+        except ValueError as e:
+            self.process_errors.append(e.args[0])
+
+    def process_data(self, value):
+        """
+        Process the Python data applied to this field and store the result.
+
+        This will be called during form construction by the form's `kwargs` or
+        `obj` argument.
+
+        :param value: The python object containing the value to process.
+        """
+        self.data = value
+
+    def process_formdata(self, valuelist):
+        """
+        Process data received over the wire from a form.
+
+        This will be called during form construction with data supplied
+        through the `formdata` argument.
+
+        :param valuelist: A list of strings to process.
+        """
+        if valuelist:
+            self.data = valuelist[0]
+
+    def populate_obj(self, obj, name):
+        """
+        Populates `obj.<name>` with the field's data.
+
+        :note: This is a destructive operation. If `obj.<name>` already exists,
+               it will be overridden. Use with caution.
+        """
+        setattr(obj, name, self.data)
+
+
+class UnboundField(object):
+    _formfield = True
+    creation_counter = 0
+
+    def __init__(self, field_class, *args, **kwargs):
+        UnboundField.creation_counter += 1
+        self.field_class = field_class
+        self.args = args
+        self.kwargs = kwargs
+        self.creation_counter = UnboundField.creation_counter
+
+    def bind(self, form, name, prefix='', translations=None, **kwargs):
+        kw = dict(
+            self.kwargs,
+            _form=form,
+            _prefix=prefix,
+            _name=name,
+            _translations=translations,
+            **kwargs
+        )
+        return self.field_class(*self.args, **kw)
+
+    def __repr__(self):
+        return '<UnboundField(%s, %r, %r)>' % (self.field_class.__name__, self.args, self.kwargs)
+
+
+class Flags(object):
+    """
+    Holds a set of boolean flags as attributes.
+
+    Accessing a non-existing attribute returns False for its value.
+    """
+    def __getattr__(self, name):
+        if name.startswith('_'):
+            return super(Flags, self).__getattr__(name)
+        return False
+
+    def __contains__(self, name):
+        return getattr(self, name)
+
+    def __repr__(self):
+        flags = (name for name in dir(self) if not name.startswith('_'))
+        return '<wtforms.fields.Flags: {%s}>' % ', '.join(flags)
+
+
+class Label(object):
+    """
+    An HTML form label.
+    """
+    def __init__(self, field_id, text):
+        self.field_id = field_id
+        self.text = text
+
+    def __str__(self):
+        return self()
+
+    def __unicode__(self):
+        return self()
+
+    def __html__(self):
+        return self()
+
+    def __call__(self, text=None, **kwargs):
+        if 'for_' in kwargs:
+            kwargs['for'] = kwargs.pop('for_')
+        else:
+            kwargs.setdefault('for', self.field_id)
+
+        attributes = widgets.html_params(**kwargs)
+        return widgets.HTMLString('<label %s>%s</label>' % (attributes, text or self.text))
+
+    def __repr__(self):
+        return 'Label(%r, %r)' % (self.field_id, self.text)
+
+
+class SelectFieldBase(Field):
+    option_widget = widgets.Option()
+
+    """
+    Base class for fields which can be iterated to produce options.
+
+    This isn't a field, but an abstract base class for fields which want to
+    provide this functionality.
+    """
+    def __init__(self, label=None, validators=None, option_widget=None, **kwargs):
+        super(SelectFieldBase, self).__init__(label, validators, **kwargs)
+
+        if option_widget is not None:
+            self.option_widget = option_widget
+
+    def iter_choices(self):
+        """
+        Provides data for choice widget rendering. Must return a sequence or
+        iterable of (value, label, selected) tuples.
+        """
+        raise NotImplementedError()
+
+    def __iter__(self):
+        opts = dict(widget=self.option_widget, _name=self.name, _form=None, _meta=self.meta)
+        for i, (value, label, checked) in enumerate(self.iter_choices()):
+            opt = self._Option(label=label, id='%s-%d' % (self.id, i), **opts)
+            opt.process(None, value)
+            opt.checked = checked
+            yield opt
+
+    class _Option(Field):
+        checked = False
+
+        def _value(self):
+            return text_type(self.data)
+
+
+class SelectField(SelectFieldBase):
+    widget = widgets.Select()
+
+    def __init__(self, label=None, validators=None, coerce=text_type, choices=None, **kwargs):
+        super(SelectField, self).__init__(label, validators, **kwargs)
+        self.coerce = coerce
+        self.choices = copy(choices)
+
+    def iter_choices(self):
+        for value, label in self.choices:
+            yield (value, label, self.coerce(value) == self.data)
+
+    def process_data(self, value):
+        try:
+            self.data = self.coerce(value)
+        except (ValueError, TypeError):
+            self.data = None
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            try:
+                self.data = self.coerce(valuelist[0])
+            except ValueError:
+                raise ValueError(self.gettext('Invalid Choice: could not coerce'))
+
+    def pre_validate(self, form):
+        for v, _ in self.choices:
+            if self.data == v:
+                break
+        else:
+            raise ValueError(self.gettext('Not a valid choice'))
+
+
+class SelectMultipleField(SelectField):
+    """
+    No different from a normal select field, except this one can take (and
+    validate) multiple choices.  You'll need to specify the HTML `size`
+    attribute to the select field when rendering.
+    """
+    widget = widgets.Select(multiple=True)
+
+    def iter_choices(self):
+        for value, label in self.choices:
+            selected = self.data is not None and self.coerce(value) in self.data
+            yield (value, label, selected)
+
+    def process_data(self, value):
+        try:
+            self.data = list(self.coerce(v) for v in value)
+        except (ValueError, TypeError):
+            self.data = None
+
+    def process_formdata(self, valuelist):
+        try:
+            self.data = list(self.coerce(x) for x in valuelist)
+        except ValueError:
+            raise ValueError(self.gettext('Invalid choice(s): one or more data inputs could not be coerced'))
+
+    def pre_validate(self, form):
+        if self.data:
+            values = list(c[0] for c in self.choices)
+            for d in self.data:
+                if d not in values:
+                    raise ValueError(self.gettext("'%(value)s' is not a valid choice for this field") % dict(value=d))
+
+
+class RadioField(SelectField):
+    """
+    Like a SelectField, except displays a list of radio buttons.
+
+    Iterating the field will produce subfields (each containing a label as
+    well) in order to allow custom rendering of the individual radio fields.
+    """
+    widget = widgets.ListWidget(prefix_label=False)
+    option_widget = widgets.RadioInput()
+
+
+class StringField(Field):
+    """
+    This field is the base for most of the more complicated fields, and
+    represents an ``<input type="text">``.
+    """
+    widget = widgets.TextInput()
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            self.data = valuelist[0]
+        elif self.data is None:
+            self.data = ''
+
+    def _value(self):
+        return text_type(self.data) if self.data is not None else ''
+
+
+class LocaleAwareNumberField(Field):
+    """
+    Base class for implementing locale-aware number parsing.
+
+    Locale-aware numbers require the 'babel' package to be present.
+    """
+    def __init__(self, label=None, validators=None, use_locale=False, number_format=None, **kwargs):
+        super(LocaleAwareNumberField, self).__init__(label, validators, **kwargs)
+        self.use_locale = use_locale
+        if use_locale:
+            self.number_format = number_format
+            self.locale = kwargs['_form'].meta.locales[0]
+            self._init_babel()
+
+    def _init_babel(self):
+        try:
+            from babel import numbers
+            self.babel_numbers = numbers
+        except ImportError:
+            raise ImportError('Using locale-aware decimals requires the babel library.')
+
+    def _parse_decimal(self, value):
+        return self.babel_numbers.parse_decimal(value, self.locale)
+
+    def _format_decimal(self, value):
+        return self.babel_numbers.format_decimal(value, self.number_format, self.locale)
+
+
+class IntegerField(Field):
+    """
+    A text field, except all input is coerced to an integer.  Erroneous input
+    is ignored and will not be accepted as a value.
+    """
+    widget = widgets.TextInput()
+
+    def __init__(self, label=None, validators=None, **kwargs):
+        super(IntegerField, self).__init__(label, validators, **kwargs)
+
+    def _value(self):
+        if self.raw_data:
+            return self.raw_data[0]
+        elif self.data is not None:
+            return text_type(self.data)
+        else:
+            return ''
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            try:
+                self.data = int(valuelist[0])
+            except ValueError:
+                self.data = None
+                raise ValueError(self.gettext('Not a valid integer value'))
+
+
+class DecimalField(LocaleAwareNumberField):
+    """
+    A text field which displays and coerces data of the `decimal.Decimal` type.
+
+    :param places:
+        How many decimal places to quantize the value to for display on form.
+        If None, does not quantize value.
+    :param rounding:
+        How to round the value during quantize, for example
+        `decimal.ROUND_UP`. If unset, uses the rounding value from the
+        current thread's context.
+    :param use_locale:
+        If True, use locale-based number formatting. Locale-based number
+        formatting requires the 'babel' package.
+    :param number_format:
+        Optional number format for locale. If omitted, use the default decimal
+        format for the locale.
+    """
+    widget = widgets.TextInput()
+
+    def __init__(self, label=None, validators=None, places=unset_value, rounding=None, **kwargs):
+        super(DecimalField, self).__init__(label, validators, **kwargs)
+        if self.use_locale and (places is not unset_value or rounding is not None):
+            raise TypeError("When using locale-aware numbers, 'places' and 'rounding' are ignored.")
+
+        if places is unset_value:
+            places = 2
+        self.places = places
+        self.rounding = rounding
+
+    def _value(self):
+        if self.raw_data:
+            return self.raw_data[0]
+        elif self.data is not None:
+            if self.use_locale:
+                return text_type(self._format_decimal(self.data))
+            elif self.places is not None:
+                if hasattr(self.data, 'quantize'):
+                    exp = decimal.Decimal('.1') ** self.places
+                    if self.rounding is None:
+                        quantized = self.data.quantize(exp)
+                    else:
+                        quantized = self.data.quantize(exp, rounding=self.rounding)
+                    return text_type(quantized)
+                else:
+                    # If for some reason, data is a float or int, then format
+                    # as we would for floats using string formatting.
+                    format = '%%0.%df' % self.places
+                    return format % self.data
+            else:
+                return text_type(self.data)
+        else:
+            return ''
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            try:
+                if self.use_locale:
+                    self.data = self._parse_decimal(valuelist[0])
+                else:
+                    self.data = decimal.Decimal(valuelist[0])
+            except (decimal.InvalidOperation, ValueError):
+                self.data = None
+                raise ValueError(self.gettext('Not a valid decimal value'))
+
+
+class FloatField(Field):
+    """
+    A text field, except all input is coerced to an float.  Erroneous input
+    is ignored and will not be accepted as a value.
+    """
+    widget = widgets.TextInput()
+
+    def __init__(self, label=None, validators=None, **kwargs):
+        super(FloatField, self).__init__(label, validators, **kwargs)
+
+    def _value(self):
+        if self.raw_data:
+            return self.raw_data[0]
+        elif self.data is not None:
+            return text_type(self.data)
+        else:
+            return ''
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            try:
+                self.data = float(valuelist[0])
+            except ValueError:
+                self.data = None
+                raise ValueError(self.gettext('Not a valid float value'))
+
+
+class BooleanField(Field):
+    """
+    Represents an ``<input type="checkbox">``. Set the ``checked``-status by using the
+    ``default``-option. Any value for ``default``, e.g. ``default="checked"`` puts
+    ``checked`` into the html-element and sets the ``data`` to ``True``
+
+    :param false_values:
+        If provided, a sequence of strings each of which is an exact match
+        string of what is considered a "false" value. Defaults to the tuple
+        ``('false', '')``
+    """
+    widget = widgets.CheckboxInput()
+    false_values = (False, 'false', '')
+
+    def __init__(self, label=None, validators=None, false_values=None, **kwargs):
+        super(BooleanField, self).__init__(label, validators, **kwargs)
+        if false_values is not None:
+            self.false_values = false_values
+
+    def process_data(self, value):
+        self.data = bool(value)
+
+    def process_formdata(self, valuelist):
+        if not valuelist or valuelist[0] in self.false_values:
+            self.data = False
+        else:
+            self.data = True
+
+    def _value(self):
+        if self.raw_data:
+            return text_type(self.raw_data[0])
+        else:
+            return 'y'
+
+
+class DateTimeField(Field):
+    """
+    A text field which stores a `datetime.datetime` matching a format.
+    """
+    widget = widgets.TextInput()
+
+    def __init__(self, label=None, validators=None, format='%Y-%m-%d %H:%M:%S', **kwargs):
+        super(DateTimeField, self).__init__(label, validators, **kwargs)
+        self.format = format
+
+    def _value(self):
+        if self.raw_data:
+            return ' '.join(self.raw_data)
+        else:
+            return self.data and self.data.strftime(self.format) or ''
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            date_str = ' '.join(valuelist)
+            try:
+                self.data = datetime.datetime.strptime(date_str, self.format)
+            except ValueError:
+                self.data = None
+                raise ValueError(self.gettext('Not a valid datetime value'))
+
+
+class DateField(DateTimeField):
+    """
+    Same as DateTimeField, except stores a `datetime.date`.
+    """
+    def __init__(self, label=None, validators=None, format='%Y-%m-%d', **kwargs):
+        super(DateField, self).__init__(label, validators, format, **kwargs)
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            date_str = ' '.join(valuelist)
+            try:
+                self.data = datetime.datetime.strptime(date_str, self.format).date()
+            except ValueError:
+                self.data = None
+                raise ValueError(self.gettext('Not a valid date value'))
+
+
+class TimeField(DateTimeField):
+    """
+    Same as DateTimeField, except stores a `time`.
+    """
+    def __init__(self, label=None, validators=None, format='%H:%M', **kwargs):
+        super(TimeField, self).__init__(label, validators, format, **kwargs)
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            time_str = ' '.join(valuelist)
+            try:
+                self.data = datetime.datetime.strptime(time_str, self.format).time()
+            except ValueError:
+                self.data = None
+                raise ValueError(self.gettext('Not a valid time value'))
+
+
+class FormField(Field):
+    """
+    Encapsulate a form as a field in another form.
+
+    :param form_class:
+        A subclass of Form that will be encapsulated.
+    :param separator:
+        A string which will be suffixed to this field's name to create the
+        prefix to enclosed fields. The default is fine for most uses.
+    """
+    widget = widgets.TableWidget()
+
+    def __init__(self, form_class, label=None, validators=None, separator='-', **kwargs):
+        super(FormField, self).__init__(label, validators, **kwargs)
+        self.form_class = form_class
+        self.separator = separator
+        self._obj = None
+        if self.filters:
+            raise TypeError('FormField cannot take filters, as the encapsulated data is not mutable.')
+        if validators:
+            raise TypeError('FormField does not accept any validators. Instead, define them on the enclosed form.')
+
+    def process(self, formdata, data=unset_value):
+        if data is unset_value:
+            try:
+                data = self.default()
+            except TypeError:
+                data = self.default
+            self._obj = data
+
+        self.object_data = data
+
+        prefix = self.name + self.separator
+        if isinstance(data, dict):
+            self.form = self.form_class(formdata=formdata, prefix=prefix, **data)
+        else:
+            self.form = self.form_class(formdata=formdata, obj=data, prefix=prefix)
+
+    def validate(self, form, extra_validators=tuple()):
+        if extra_validators:
+            raise TypeError('FormField does not accept in-line validators, as it gets errors from the enclosed form.')
+        return self.form.validate()
+
+    def populate_obj(self, obj, name):
+        candidate = getattr(obj, name, None)
+        if candidate is None:
+            if self._obj is None:
+                raise TypeError('populate_obj: cannot find a value to populate from the provided obj or input data/defaults')
+            candidate = self._obj
+            setattr(obj, name, candidate)
+
+        self.form.populate_obj(candidate)
+
+    def __iter__(self):
+        return iter(self.form)
+
+    def __getitem__(self, name):
+        return self.form[name]
+
+    def __getattr__(self, name):
+        return getattr(self.form, name)
+
+    @property
+    def data(self):
+        return self.form.data
+
+    @property
+    def errors(self):
+        return self.form.errors
+
+
+class FieldList(Field):
+    """
+    Encapsulate an ordered list of multiple instances of the same field type,
+    keeping data as a list.
+
+    >>> authors = FieldList(StringField('Name', [validators.required()]))
+
+    :param unbound_field:
+        A partially-instantiated field definition, just like that would be
+        defined on a form directly.
+    :param min_entries:
+        if provided, always have at least this many entries on the field,
+        creating blank ones if the provided input does not specify a sufficient
+        amount.
+    :param max_entries:
+        accept no more than this many entries as input, even if more exist in
+        formdata.
+    """
+    widget = widgets.ListWidget()
+
+    def __init__(self, unbound_field, label=None, validators=None, min_entries=0,
+                 max_entries=None, default=tuple(), **kwargs):
+        super(FieldList, self).__init__(label, validators, default=default, **kwargs)
+        if self.filters:
+            raise TypeError('FieldList does not accept any filters. Instead, define them on the enclosed field.')
+        assert isinstance(unbound_field, UnboundField), 'Field must be unbound, not a field class'
+        self.unbound_field = unbound_field
+        self.min_entries = min_entries
+        self.max_entries = max_entries
+        self.last_index = -1
+        self._prefix = kwargs.get('_prefix', '')
+
+    def process(self, formdata, data=unset_value):
+        self.entries = []
+        if data is unset_value or not data:
+            try:
+                data = self.default()
+            except TypeError:
+                data = self.default
+
+        self.object_data = data
+
+        if formdata:
+            indices = sorted(set(self._extract_indices(self.name, formdata)))
+            if self.max_entries:
+                indices = indices[:self.max_entries]
+
+            idata = iter(data)
+            for index in indices:
+                try:
+                    obj_data = next(idata)
+                except StopIteration:
+                    obj_data = unset_value
+                self._add_entry(formdata, obj_data, index=index)
+        else:
+            for obj_data in data:
+                self._add_entry(formdata, obj_data)
+
+        while len(self.entries) < self.min_entries:
+            self._add_entry(formdata)
+
+    def _extract_indices(self, prefix, formdata):
+        """
+        Yield indices of any keys with given prefix.
+
+        formdata must be an object which will produce keys when iterated.  For
+        example, if field 'foo' contains keys 'foo-0-bar', 'foo-1-baz', then
+        the numbers 0 and 1 will be yielded, but not neccesarily in order.
+        """
+        offset = len(prefix) + 1
+        for k in formdata:
+            if k.startswith(prefix):
+                k = k[offset:].split('-', 1)[0]
+                if k.isdigit():
+                    yield int(k)
+
+    def validate(self, form, extra_validators=tuple()):
+        """
+        Validate this FieldList.
+
+        Note that FieldList validation differs from normal field validation in
+        that FieldList validates all its enclosed fields first before running any
+        of its own validators.
+        """
+        self.errors = []
+
+        # Run validators on all entries within
+        for subfield in self.entries:
+            if not subfield.validate(form):
+                self.errors.append(subfield.errors)
+
+        chain = itertools.chain(self.validators, extra_validators)
+        self._run_validation_chain(form, chain)
+
+        return len(self.errors) == 0
+
+    def populate_obj(self, obj, name):
+        values = getattr(obj, name, None)
+        try:
+            ivalues = iter(values)
+        except TypeError:
+            ivalues = iter([])
+
+        candidates = itertools.chain(ivalues, itertools.repeat(None))
+        _fake = type(str('_fake'), (object, ), {})
+        output = []
+        for field, data in izip(self.entries, candidates):
+            fake_obj = _fake()
+            fake_obj.data = data
+            field.populate_obj(fake_obj, 'data')
+            output.append(fake_obj.data)
+
+        setattr(obj, name, output)
+
+    def _add_entry(self, formdata=None, data=unset_value, index=None):
+        assert not self.max_entries or len(self.entries) < self.max_entries, \
+            'You cannot have more than max_entries entries in this FieldList'
+        if index is None:
+            index = self.last_index + 1
+        self.last_index = index
+        name = '%s-%d' % (self.short_name, index)
+        id = '%s-%d' % (self.id, index)
+        field = self.unbound_field.bind(form=None, name=name, prefix=self._prefix, id=id, _meta=self.meta,
+                                        translations=self._translations)
+        field.process(formdata, data)
+        self.entries.append(field)
+        return field
+
+    def append_entry(self, data=unset_value):
+        """
+        Create a new entry with optional default data.
+
+        Entries added in this way will *not* receive formdata however, and can
+        only receive object data.
+        """
+        return self._add_entry(data=data)
+
+    def pop_entry(self):
+        """ Removes the last entry from the list and returns it. """
+        entry = self.entries.pop()
+        self.last_index -= 1
+        return entry
+
+    def __iter__(self):
+        return iter(self.entries)
+
+    def __len__(self):
+        return len(self.entries)
+
+    def __getitem__(self, index):
+        return self.entries[index]
+
+    @property
+    def data(self):
+        return [f.data for f in self.entries]
+
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_sources/AIPscan.API.rst.txt b/docs/_build/html/_sources/AIPscan.API.rst.txt new file mode 100644 index 00000000..7f30ec71 --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.API.rst.txt @@ -0,0 +1,37 @@ +AIPscan.API package +=================== + +Submodules +---------- + +AIPscan.API.namespace\_data module +---------------------------------- + +.. automodule:: AIPscan.API.namespace_data + :members: + :undoc-members: + :show-inheritance: + +AIPscan.API.namespace\_infos module +----------------------------------- + +.. automodule:: AIPscan.API.namespace_infos + :members: + :undoc-members: + :show-inheritance: + +AIPscan.API.views module +------------------------ + +.. automodule:: AIPscan.API.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.API + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.Aggregator.rst.txt b/docs/_build/html/_sources/AIPscan.Aggregator.rst.txt new file mode 100644 index 00000000..9acf8653 --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.Aggregator.rst.txt @@ -0,0 +1,85 @@ +AIPscan.Aggregator package +========================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + AIPscan.Aggregator.tests + +Submodules +---------- + +AIPscan.Aggregator.celery\_helpers module +----------------------------------------- + +.. automodule:: AIPscan.Aggregator.celery_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.database\_helpers module +------------------------------------------- + +.. automodule:: AIPscan.Aggregator.database_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.forms module +------------------------------- + +.. automodule:: AIPscan.Aggregator.forms + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.mets\_parse\_helpers module +---------------------------------------------- + +.. automodule:: AIPscan.Aggregator.mets_parse_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.task\_helpers module +--------------------------------------- + +.. automodule:: AIPscan.Aggregator.task_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tasks module +------------------------------- + +.. automodule:: AIPscan.Aggregator.tasks + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.types module +------------------------------- + +.. automodule:: AIPscan.Aggregator.types + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.views module +------------------------------- + +.. automodule:: AIPscan.Aggregator.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Aggregator + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.Aggregator.tests.rst.txt b/docs/_build/html/_sources/AIPscan.Aggregator.tests.rst.txt new file mode 100644 index 00000000..282a1a1b --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.Aggregator.tests.rst.txt @@ -0,0 +1,45 @@ +AIPscan.Aggregator.tests package +================================ + +Submodules +---------- + +AIPscan.Aggregator.tests.test\_database\_helpers module +------------------------------------------------------- + +.. automodule:: AIPscan.Aggregator.tests.test_database_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tests.test\_mets module +------------------------------------------ + +.. automodule:: AIPscan.Aggregator.tests.test_mets + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tests.test\_task\_helpers module +--------------------------------------------------- + +.. automodule:: AIPscan.Aggregator.tests.test_task_helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Aggregator.tests.test\_types module +------------------------------------------- + +.. automodule:: AIPscan.Aggregator.tests.test_types + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Aggregator.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.Data.rst.txt b/docs/_build/html/_sources/AIPscan.Data.rst.txt new file mode 100644 index 00000000..af61dfd2 --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.Data.rst.txt @@ -0,0 +1,29 @@ +AIPscan.Data package +==================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + AIPscan.Data.tests + +Submodules +---------- + +AIPscan.Data.data module +------------------------ + +.. automodule:: AIPscan.Data.data + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Data + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.Data.tests.rst.txt b/docs/_build/html/_sources/AIPscan.Data.tests.rst.txt new file mode 100644 index 00000000..248701db --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.Data.tests.rst.txt @@ -0,0 +1,21 @@ +AIPscan.Data.tests package +========================== + +Submodules +---------- + +AIPscan.Data.tests.test\_largest\_files module +---------------------------------------------- + +.. automodule:: AIPscan.Data.tests.test_largest_files + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Data.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.Home.rst.txt b/docs/_build/html/_sources/AIPscan.Home.rst.txt new file mode 100644 index 00000000..605a4308 --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.Home.rst.txt @@ -0,0 +1,21 @@ +AIPscan.Home package +==================== + +Submodules +---------- + +AIPscan.Home.views module +------------------------- + +.. automodule:: AIPscan.Home.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Home + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.Reporter.rst.txt b/docs/_build/html/_sources/AIPscan.Reporter.rst.txt new file mode 100644 index 00000000..cd0d31f6 --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.Reporter.rst.txt @@ -0,0 +1,61 @@ +AIPscan.Reporter package +======================== + +Submodules +---------- + +AIPscan.Reporter.helpers module +------------------------------- + +.. automodule:: AIPscan.Reporter.helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_aip\_contents module +--------------------------------------------- + +.. automodule:: AIPscan.Reporter.report_aip_contents + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_formats\_count module +---------------------------------------------- + +.. automodule:: AIPscan.Reporter.report_formats_count + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_largest\_files module +---------------------------------------------- + +.. automodule:: AIPscan.Reporter.report_largest_files + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.report\_originals\_with\_derivatives module +------------------------------------------------------------ + +.. automodule:: AIPscan.Reporter.report_originals_with_derivatives + :members: + :undoc-members: + :show-inheritance: + +AIPscan.Reporter.views module +----------------------------- + +.. automodule:: AIPscan.Reporter.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.Reporter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.User.rst.txt b/docs/_build/html/_sources/AIPscan.User.rst.txt new file mode 100644 index 00000000..71478bb4 --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.User.rst.txt @@ -0,0 +1,29 @@ +AIPscan.User package +==================== + +Submodules +---------- + +AIPscan.User.forms module +------------------------- + +.. automodule:: AIPscan.User.forms + :members: + :undoc-members: + :show-inheritance: + +AIPscan.User.views module +------------------------- + +.. automodule:: AIPscan.User.views + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan.User + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/AIPscan.rst.txt b/docs/_build/html/_sources/AIPscan.rst.txt new file mode 100644 index 00000000..5ea3b0db --- /dev/null +++ b/docs/_build/html/_sources/AIPscan.rst.txt @@ -0,0 +1,74 @@ +AIPscan package +=============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + AIPscan.API + AIPscan.Aggregator + AIPscan.Data + AIPscan.Home + AIPscan.Reporter + AIPscan.User + +Submodules +---------- + +AIPscan.celery module +--------------------- + +.. automodule:: AIPscan.celery + :members: + :undoc-members: + :show-inheritance: + +AIPscan.conftest module +----------------------- + +.. automodule:: AIPscan.conftest + :members: + :undoc-members: + :show-inheritance: + +AIPscan.extensions module +------------------------- + +.. automodule:: AIPscan.extensions + :members: + :undoc-members: + :show-inheritance: + +AIPscan.helpers module +---------------------- + +.. automodule:: AIPscan.helpers + :members: + :undoc-members: + :show-inheritance: + +AIPscan.models module +--------------------- + +.. automodule:: AIPscan.models + :members: + :undoc-members: + :show-inheritance: + +AIPscan.worker module +--------------------- + +.. automodule:: AIPscan.worker + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: AIPscan + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/index.rst.txt b/docs/_build/html/_sources/index.rst.txt new file mode 100644 index 00000000..7d1a1afe --- /dev/null +++ b/docs/_build/html/_sources/index.rst.txt @@ -0,0 +1,30 @@ +.. AIPscan documentation master file, created by + sphinx-quickstart on Fri Nov 13 14:04:02 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to AIPscan's documentation! +=================================== + +AIPscan is a utility developed by Artefactual Systems Inc. The utility +is currently a web-app that can retrieve the contents of the AIPs +stored in multiple storage services to then convert the information +they contain into a database serialization which can support repository +management and digital preservation planning. + +The contents of this package are described below. Some additional work +is needed to improve this! + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + + overview + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/_build/html/_sources/modules.rst.txt b/docs/_build/html/_sources/modules.rst.txt new file mode 100644 index 00000000..e1486566 --- /dev/null +++ b/docs/_build/html/_sources/modules.rst.txt @@ -0,0 +1,11 @@ +AIPscan +======= + +.. toctree:: + :maxdepth: 4 + + AIPscan + AIPscan.API + AIPscan.Data + AIPscan.Aggregator + AIPscan.Reporter diff --git a/docs/_build/html/_sources/overview.rst.txt b/docs/_build/html/_sources/overview.rst.txt new file mode 100644 index 00000000..16e6296e --- /dev/null +++ b/docs/_build/html/_sources/overview.rst.txt @@ -0,0 +1,4 @@ +Overview +======== + +This is an overview of the package. diff --git a/docs/_build/html/_static/alabaster.css b/docs/_build/html/_static/alabaster.css new file mode 100644 index 00000000..4c500fb9 --- /dev/null +++ b/docs/_build/html/_static/alabaster.css @@ -0,0 +1,707 @@ +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: -20px -30px 20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} +@media screen and (min-width: 876px) { + div.sphinxsidebar { + position: fixed; + margin-left: 0; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Make nested-list/multi-paragraph items look better in Releases changelog + * pages. Without this, docutils' magical list fuckery causes inconsistent + * formatting between different release sub-lists. + */ +div#changelog > div.section > ul > li > p:only-child { + margin-bottom: 0; +} + +/* Hide fugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css new file mode 100644 index 00000000..24a49f09 --- /dev/null +++ b/docs/_build/html/_static/basic.css @@ -0,0 +1,856 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a.brackets:before, +span.brackets > a:before{ + content: "["; +} + +a.brackets:after, +span.brackets > a:after { + content: "]"; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +dl.footnote > dt, +dl.citation > dt { + float: left; + margin-right: 0.5em; +} + +dl.footnote > dd, +dl.citation > dd { + margin-bottom: 0em; +} + +dl.footnote > dd:after, +dl.citation > dd:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dt:after { + content: ":"; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0.5em; + content: ":"; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +code.descclassname { + background-color: transparent; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/custom.css b/docs/_build/html/_static/custom.css new file mode 100644 index 00000000..2a924f1d --- /dev/null +++ b/docs/_build/html/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/docs/_build/html/_static/doctools.js b/docs/_build/html/_static/doctools.js new file mode 100644 index 00000000..7d88f807 --- /dev/null +++ b/docs/_build/html/_static/doctools.js @@ -0,0 +1,316 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for all documentation. + * + * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + +/** + * make the code below compatible with browsers without + * an installed firebug like debugger +if (!window.console || !console.firebug) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", + "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", + "profile", "profileEnd"]; + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +} + */ + +/** + * small helper function to urldecode strings + */ +jQuery.urldecode = function(x) { + return decodeURIComponent(x).replace(/\+/g, ' '); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} + +/** + * Small JavaScript module for the documentation. + */ +var Documentation = { + + init : function() { + this.fixFirefoxAnchorBug(); + this.highlightSearchWords(); + this.initIndexTable(); + if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { + this.initOnKeyListeners(); + } + }, + + /** + * i18n support + */ + TRANSLATIONS : {}, + PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, + LOCALE : 'unknown', + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext : function(string) { + var translated = Documentation.TRANSLATIONS[string]; + if (typeof translated === 'undefined') + return string; + return (typeof translated === 'string') ? translated : translated[0]; + }, + + ngettext : function(singular, plural, n) { + var translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated === 'undefined') + return (n == 1) ? singular : plural; + return translated[Documentation.PLURALEXPR(n)]; + }, + + addTranslations : function(catalog) { + for (var key in catalog.messages) + this.TRANSLATIONS[key] = catalog.messages[key]; + this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); + this.LOCALE = catalog.locale; + }, + + /** + * add context elements like header anchor links + */ + addContextElements : function() { + $('div[id] > :header:first').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this definition')). + appendTo(this); + }); + }, + + /** + * workaround a firefox stupidity + * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 + */ + fixFirefoxAnchorBug : function() { + if (document.location.hash && $.browser.mozilla) + window.setTimeout(function() { + document.location.href += ''; + }, 10); + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords : function() { + var params = $.getQueryParameters(); + var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + if (terms.length) { + var body = $('div.body'); + if (!body.length) { + body = $('body'); + } + window.setTimeout(function() { + $.each(terms, function() { + body.highlightText(this.toLowerCase(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) === 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this === '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + }, + + initOnKeyListeners: function() { + $(document).keydown(function(event) { + var activeElementType = document.activeElement.tagName; + // don't navigate when in search box, textarea, dropdown or button + if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' + && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey + && !event.shiftKey) { + switch (event.keyCode) { + case 37: // left + var prevHref = $('link[rel="prev"]').prop('href'); + if (prevHref) { + window.location.href = prevHref; + return false; + } + case 39: // right + var nextHref = $('link[rel="next"]').prop('href'); + if (nextHref) { + window.location.href = nextHref; + return false; + } + } + } + }); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/docs/_build/html/_static/documentation_options.js b/docs/_build/html/_static/documentation_options.js new file mode 100644 index 00000000..998c4037 --- /dev/null +++ b/docs/_build/html/_static/documentation_options.js @@ -0,0 +1,12 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '0.x', + LANGUAGE: 'None', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false +}; \ No newline at end of file diff --git a/docs/_build/html/_static/file.png b/docs/_build/html/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..a858a410e4faa62ce324d814e4b816fff83a6fb3 GIT binary patch literal 286 zcmV+(0pb3MP)s`hMrGg#P~ix$^RISR_I47Y|r1 z_CyJOe}D1){SET-^Amu_i71Lt6eYfZjRyw@I6OQAIXXHDfiX^GbOlHe=Ae4>0m)d(f|Me07*qoM6N<$f}vM^LjV8( literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/jquery-3.5.1.js b/docs/_build/html/_static/jquery-3.5.1.js new file mode 100644 index 00000000..50937333 --- /dev/null +++ b/docs/_build/html/_static/jquery-3.5.1.js @@ -0,0 +1,10872 @@ +/*! + * jQuery JavaScript Library v3.5.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2020-05-04T22:49Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + return typeof obj === "function" && typeof obj.nodeType !== "number"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.5.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.5 + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://js.foundation/ + * + * Date: 2020-03-14 + */ +( function( window ) { +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ( {} ).hasOwnProperty, + arr = [], + pop = arr.pop, + pushNative = arr.push, + push = arr.push, + slice = arr.slice, + + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[ i ] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rhtml = /HTML$/i, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android<4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + + // Can't trust NodeList.length + while ( ( target[ j++ ] = els[ i++ ] ) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && + + // Support: IE 8 only + // Exclude object elements + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split( "|" ), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[ i ] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( ( cur = cur.nextSibling ) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return ( name === "input" || name === "button" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + var namespace = elem.namespaceURI, + docElem = ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert( function( el ) { + el.className = "i"; + return !el.getAttribute( "className" ); + } ); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find[ "TAG" ] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll( "[name=d]" ).length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: Opera 10 - 11 only + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); + } + + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { + + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + } ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); + } : + function( a, b ) { + if ( b ) { + while ( ( b = b.parentNode ) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document || a.ownerDocument == preferredDoc && + contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document || b.ownerDocument == preferredDoc && + contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + return a == document ? -1 : + b == document ? 1 : + /* eslint-enable eqeqeq */ + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( ( cur = cur.parentNode ) ) { + ap.unshift( cur ); + } + cur = b; + while ( ( cur = cur.parentNode ) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[ i ] === bp[ i ] ) { + i++; + } + + return i ? + + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[ i ], bp[ i ] ) : + + // Otherwise nodes in our document sort first + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + ap[ i ] == preferredDoc ? -1 : + bp[ i ] == preferredDoc ? 1 : + /* eslint-enable eqeqeq */ + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( support.matchesSelector && documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || + match[ 5 ] || "" ).replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + + ")" + className + "(" + whitespace + "|$)" ) ) && classCache( + className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + /* eslint-disable max-len */ + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + /* eslint-enable max-len */ + + }; + }, + + "CHILD": function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + "not": markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element (issue #299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + "has": markFunction( function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + } ), + + "contains": markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && + ( !document.hasFocus || document.hasFocus() ) && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return ( nodeName === "input" && !!elem.checked ) || + ( nodeName === "option" && !!elem.selected ); + }, + + "selected": function( elem ) { + + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos[ "empty" ]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo( function() { + return [ 0 ]; + } ), + + "last": createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + "even": createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "odd": createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rcombinators.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || + ( outerCache[ elem.uniqueID ] = {} ); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = uniqueCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( + selector || "*", + context.nodeType ? [ context ] : context, + [] + ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens + .slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( + selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) + ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find[ "ID" ]( token.matches[ 0 ] + .replace( runescape, funescape ), context ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || + context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert( function( el ) { + el.innerHTML = ""; + return el.firstChild.getAttribute( "href" ) === "#"; +} ) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + } ); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert( function( el ) { + el.innerHTML = ""; + el.firstChild.setAttribute( "value", "" ); + return el.firstChild.getAttribute( "value" ) === ""; +} ) ) { + addHandle( "value", function( elem, _name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + } ); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert( function( el ) { + return el.getAttribute( "disabled" ) == null; +} ) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; + } + } ); +} + +return Sizzle; + +} )( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; +jQuery.escapeSelector = Sizzle.escape; + + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +}; +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the master Deferred + master = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + master.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( master.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return master.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); + } + + return master.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + return result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + + which: function( event ) { + var button = event.button; + + // Add which for key events + if ( event.which == null && rkeyEvent.test( event.type ) ) { + return event.charCode != null ? event.charCode : event.keyCode; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { + if ( button & 1 ) { + return 1; + } + + if ( button & 2 ) { + return 3; + } + + if ( button & 4 ) { + return 2; + } + + return 0; + } + + return event.which; + } +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + delegateType: delegateType + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (#8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px"; + tr.style.height = "1px"; + trChild.style.height = "9px"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "animationIterationCount": true, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( isValidValue ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = classesToArray( value ); + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( + dataPriv.get( cur, "events" ) || Object.create( null ) + )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) { + xml = undefined; + } + + if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ) + .filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ) + .map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script + if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( " + + + + + + + + + + + + + +
+ + +
+
+ + +
+ + +

Index

+ +
+ A + | C + | D + | E + | F + | G + | I + | L + | M + | N + | O + | P + | R + | S + | T + | U + | V + | W + +
+

A

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + + +
+ + + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html new file mode 100644 index 00000000..26f4e232 --- /dev/null +++ b/docs/_build/html/index.html @@ -0,0 +1,191 @@ + + + + + + + + Welcome to AIPscan’s documentation! — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

Welcome to AIPscan’s documentation!

+

AIPscan is a utility developed by Artefactual Systems Inc. The utility +is currently a web-app that can retrieve the contents of the AIPs +stored in multiple storage services to then convert the information +they contain into a database serialization which can support repository +management and digital preservation planning.

+

The contents of this package are described below. Some additional work +is needed to improve this!

+ +
+
+

Indices and tables

+ +
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/modules.html b/docs/_build/html/modules.html new file mode 100644 index 00000000..fa8081be --- /dev/null +++ b/docs/_build/html/modules.html @@ -0,0 +1,254 @@ + + + + + + + + AIPscan — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

AIPscan

+
+ +
+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..ef617910f2e77eb9c501b3f2fb7aaa4372236d28 GIT binary patch literal 2129 zcmV-X2(I@dAX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkGNl%ZEX>4U6X>%ZBZ*6dL zWpi_7WFU2OX>MmAdTeQ8E(&)y6%cPg9OtxRPm$&r$w zZEFW3A_;pYr@Tg9FHaKv0Jbo~=!dcXjCweo-pj3ZoW!wYpEbjr(hjE9M>yC!)O7V z>oW9njBxyxvO|h+(H!(7zsl-+0(K+?JjiR4eUgW16G(&`lE1*4vuBCKm$1ccC04`J zQ(aM9<`p@R^|jFLmcwdlROi1IyJgjCXbvn-V7u-M&0HW|&wC9;Up_uRePqlGZtz+_ z6V}=%&~m&t-6118QsZiHOL(6$p~d{tH%>(C^*45kQzI~cN$B=VT;Gq^?O_89%AB{G zk}#gm;By02u1LsLF^^@H%05yyAmeS_(_HifiBzf3(N)^D)Pz_$pL~&Rmgmyzatx*H zEun&b1nrb0nF%NbsiKghXZe|;y&IZoe1l7IEytd8k;f4in01k$E~G%38S-HAS8i4} z5m>nhyjef`p+&W%A5@>Y@6*soEBu9CXSsbx*Ar-hppQO?bhXwV9OeS4jxCTW6j zNgyl<0d4Z5u`bCROatvjXorv39GRh_f*u+s14wq9@EW`HpHtNBl*qayEEf!5MO~AP z@OqazUJ>E_{AMM32^9z^*VM48{S2Cl6nI4qJkI`^69liWKy#3(u6zrk*ymsACT<;K zF-Lu33Nn;Ltic_oDtd&MD?ZWk6xG&}Xw+16T?H;zMX9*moq0Rn?rE0vW6RC$cM!YNv-1#>$aXg$ z4V#xErD5f9_j}82HvONmJk}KbIkTVb%}HaBiW+aw@7&S1s~b+Y>YMH?kcG17GnG-| zO87WKZb_p?N}ebyT{{ULC?*GXwSSzukNG`lyxq*CYj}WE?!jE@!3@gXdSRMRY#fLA$&t4u}i zW#!!T=Z1`4MDN=Ju5)Q^i8mGKmY#F+_u_&|JXC(_jfl$Lo@$&94eIgUt9^pvRJhKG z4-5O8ltJ?S;OQ$ErfwMObfuiU<1ZV|{ zN6|1*JqVhev~c-g=PT*{Py!BqwLduQTmzGi%y`|&}n|~Q;A#K=I;E^Ez zARPfhdWI2V<|U`41Rl7)w^BN|Pu$v)qudaGua0niW?)D!GD4j*86qGj%3ih=J>>X#aaNPP9GVI2km!-w*)|j6ZreuL4B1q^ z$Ox#D941ghjzMOpLyW|Sq=N~Bb(ED|rzwn3`h05wGn{>7_DNVLzJuE2N+{6^Z=h1b z{Z%9QNqxOLKh|luc;Zi}^iGbTy_Wy}`Il$%!B(Y@=!wI3cQW25bTx1H{S5>AVj7Fl zYTAfZUEL+CP + + + + + + Overview — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+

Overview

+

This is an overview of the package.

+
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/py-modindex.html b/docs/_build/html/py-modindex.html new file mode 100644 index 00000000..75e3ada3 --- /dev/null +++ b/docs/_build/html/py-modindex.html @@ -0,0 +1,328 @@ + + + + + + + + Python Module Index — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ + +

Python Module Index

+ +
+ a +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ a
+ AIPscan +
    + AIPscan.Aggregator +
    + AIPscan.Aggregator.celery_helpers +
    + AIPscan.Aggregator.database_helpers +
    + AIPscan.Aggregator.forms +
    + AIPscan.Aggregator.mets_parse_helpers +
    + AIPscan.Aggregator.task_helpers +
    + AIPscan.Aggregator.tasks +
    + AIPscan.Aggregator.tests +
    + AIPscan.Aggregator.tests.test_database_helpers +
    + AIPscan.Aggregator.tests.test_mets +
    + AIPscan.Aggregator.tests.test_task_helpers +
    + AIPscan.Aggregator.tests.test_types +
    + AIPscan.Aggregator.types +
    + AIPscan.Aggregator.views +
    + AIPscan.API +
    + AIPscan.API.namespace_data +
    + AIPscan.API.namespace_infos +
    + AIPscan.API.views +
    + AIPscan.celery +
    + AIPscan.conftest +
    + AIPscan.Data +
    + AIPscan.Data.data +
    + AIPscan.Data.tests +
    + AIPscan.Data.tests.test_largest_files +
    + AIPscan.extensions +
    + AIPscan.helpers +
    + AIPscan.Home +
    + AIPscan.Home.views +
    + AIPscan.models +
    + AIPscan.Reporter +
    + AIPscan.Reporter.helpers +
    + AIPscan.Reporter.report_aip_contents +
    + AIPscan.Reporter.report_formats_count +
    + AIPscan.Reporter.report_largest_files +
    + AIPscan.Reporter.report_originals_with_derivatives +
    + AIPscan.Reporter.views +
    + AIPscan.User +
    + AIPscan.User.forms +
    + AIPscan.User.views +
    + AIPscan.worker +
+ + +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html new file mode 100644 index 00000000..78a26687 --- /dev/null +++ b/docs/_build/html/search.html @@ -0,0 +1,122 @@ + + + + + + + + Search — AIPscan 0.x documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +

Search

+
+ +

+ Please activate JavaScript to enable the search + functionality. +

+
+

+ Searching for multiple words only shows matches that contain + all words. +

+
+ + + +
+ +
+ +
+ +
+ +
+
+
+
+ + + + + Fork me on GitHub + + + + + + \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js new file mode 100644 index 00000000..0f15e265 --- /dev/null +++ b/docs/_build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["AIPscan","AIPscan.API","AIPscan.Aggregator","AIPscan.Aggregator.tests","AIPscan.Data","AIPscan.Data.tests","AIPscan.Home","AIPscan.Reporter","AIPscan.User","index","modules","overview"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,"sphinx.ext.viewcode":1,sphinx:56},filenames:["AIPscan.rst","AIPscan.API.rst","AIPscan.Aggregator.rst","AIPscan.Aggregator.tests.rst","AIPscan.Data.rst","AIPscan.Data.tests.rst","AIPscan.Home.rst","AIPscan.Reporter.rst","AIPscan.User.rst","index.rst","modules.rst","overview.rst"],objects:{"":{AIPscan:[0,0,0,"-"]},"AIPscan.API":{namespace_data:[1,0,0,"-"],namespace_infos:[1,0,0,"-"],views:[1,0,0,"-"]},"AIPscan.API.namespace_data":{AIPList:[1,1,1,""],DerivativeList:[1,1,1,""],FMTList:[1,1,1,""],LargestFileList:[1,1,1,""],parse_bool:[1,4,1,""]},"AIPscan.API.namespace_data.AIPList":{get:[1,2,1,""],methods:[1,3,1,""]},"AIPscan.API.namespace_data.DerivativeList":{get:[1,2,1,""],methods:[1,3,1,""]},"AIPscan.API.namespace_data.FMTList":{get:[1,2,1,""],methods:[1,3,1,""]},"AIPscan.API.namespace_data.LargestFileList":{get:[1,2,1,""],methods:[1,3,1,""]},"AIPscan.API.namespace_infos":{Version:[1,1,1,""]},"AIPscan.API.namespace_infos.Version":{get:[1,2,1,""],methods:[1,3,1,""]},"AIPscan.Aggregator":{celery_helpers:[2,0,0,"-"],database_helpers:[2,0,0,"-"],forms:[2,0,0,"-"],mets_parse_helpers:[2,0,0,"-"],task_helpers:[2,0,0,"-"],tasks:[2,0,0,"-"],tests:[3,0,0,"-"],types:[2,0,0,"-"],views:[2,0,0,"-"]},"AIPscan.Aggregator.celery_helpers":{write_celery_update:[2,4,1,""]},"AIPscan.Aggregator.database_helpers":{collect_mets_agents:[2,4,1,""],create_agent_objects:[2,4,1,""],create_aip_object:[2,4,1,""],create_event_objects:[2,4,1,""],process_aip_data:[2,4,1,""]},"AIPscan.Aggregator.forms":{StorageServiceForm:[2,1,1,""]},"AIPscan.Aggregator.forms.StorageServiceForm":{"default":[2,3,1,""],api_key:[2,3,1,""],download_limit:[2,3,1,""],download_offset:[2,3,1,""],name:[2,3,1,""],url:[2,3,1,""],user_name:[2,3,1,""]},"AIPscan.Aggregator.mets_parse_helpers":{METSError:[2,5,1,""],get_aip_original_name:[2,4,1,""],parse_mets_with_metsrw:[2,4,1,""]},"AIPscan.Aggregator.task_helpers":{create_numbered_subdirs:[2,4,1,""],download_mets:[2,4,1,""],format_api_url_with_limit_offset:[2,4,1,""],get_mets_url:[2,4,1,""],get_packages_directory:[2,4,1,""],process_package_object:[2,4,1,""]},"AIPscan.Aggregator.tasks":{TaskError:[2,5,1,""],parse_packages_and_load_mets:[2,4,1,""],start_mets_task:[2,4,1,""],write_packages_json:[2,4,1,""]},"AIPscan.Aggregator.tests":{test_database_helpers:[3,0,0,"-"],test_mets:[3,0,0,"-"],test_task_helpers:[3,0,0,"-"],test_types:[3,0,0,"-"]},"AIPscan.Aggregator.tests.test_database_helpers":{test_collect_agents:[3,4,1,""],test_create_aip:[3,4,1,""],test_event_creation:[3,4,1,""]},"AIPscan.Aggregator.tests.test_mets":{test_get_aip_original_name:[3,4,1,""]},"AIPscan.Aggregator.tests.test_task_helpers":{packages:[3,4,1,""],test_create_numbered_subdirs:[3,4,1,""],test_format_api_url:[3,4,1,""],test_get_mets_url:[3,4,1,""],test_process_package_object:[3,4,1,""],test_tz_neutral_dates:[3,4,1,""]},"AIPscan.Aggregator.tests.test_types":{test_get_relative_path:[3,4,1,""],test_package_ness:[3,4,1,""],test_storage_service_package_eq:[3,4,1,""],test_storage_service_package_init:[3,4,1,""]},"AIPscan.Aggregator.types":{PackageError:[2,5,1,""],StorageServicePackage:[2,1,1,""]},"AIPscan.Aggregator.types.StorageServicePackage":{compressed_ext:[2,3,1,""],default_pair_tree:[2,3,1,""],get_relative_path:[2,2,1,""],is_aip:[2,2,1,""],is_deleted:[2,2,1,""],is_dip:[2,2,1,""],is_replica:[2,2,1,""],is_sip:[2,2,1,""]},"AIPscan.Aggregator.views":{delete_fetch_job:[2,4,1,""],delete_storage_service:[2,4,1,""],edit_storage_service:[2,4,1,""],get_mets_task_status:[2,4,1,""],new_fetch_job:[2,4,1,""],new_storage_service:[2,4,1,""],ss:[2,4,1,""],ss_default:[2,4,1,""],storage_service:[2,4,1,""],task_status:[2,4,1,""]},"AIPscan.Data":{data:[4,0,0,"-"],tests:[5,0,0,"-"]},"AIPscan.Data.data":{aip_overview:[4,4,1,""],aip_overview_two:[4,4,1,""],derivative_overview:[4,4,1,""],largest_files:[4,4,1,""]},"AIPscan.Data.tests":{test_largest_files:[5,0,0,"-"]},"AIPscan.Data.tests.test_largest_files":{test_largest_files:[5,4,1,""],test_largest_files_elements:[5,4,1,""]},"AIPscan.Home":{views:[6,0,0,"-"]},"AIPscan.Home.views":{index:[6,4,1,""]},"AIPscan.Reporter":{helpers:[7,0,0,"-"],report_aip_contents:[7,0,0,"-"],report_formats_count:[7,0,0,"-"],report_largest_files:[7,0,0,"-"],report_originals_with_derivatives:[7,0,0,"-"],views:[7,0,0,"-"]},"AIPscan.Reporter.helpers":{translate_headers:[7,4,1,""]},"AIPscan.Reporter.report_aip_contents":{aip_contents:[7,4,1,""]},"AIPscan.Reporter.report_formats_count":{chart_formats_count:[7,4,1,""],plot_formats_count:[7,4,1,""],report_formats_count:[7,4,1,""]},"AIPscan.Reporter.report_largest_files":{largest_files:[7,4,1,""]},"AIPscan.Reporter.report_originals_with_derivatives":{original_derivatives:[7,4,1,""]},"AIPscan.Reporter.views":{reports:[7,4,1,""],view_aip:[7,4,1,""],view_aips:[7,4,1,""],view_file:[7,4,1,""]},"AIPscan.User":{forms:[8,0,0,"-"],views:[8,0,0,"-"]},"AIPscan.User.forms":{LoginForm:[8,1,1,""]},"AIPscan.User.forms.LoginForm":{password:[8,3,1,""],remember_me:[8,3,1,""],submit:[8,3,1,""],username:[8,3,1,""]},"AIPscan.celery":{configure_celery:[0,4,1,""]},"AIPscan.conftest":{app_instance:[0,4,1,""]},"AIPscan.helpers":{get_human_readable_file_size:[0,4,1,""]},"AIPscan.models":{AIP:[0,1,1,""],Agent:[0,1,1,""],Event:[0,1,1,""],FetchJob:[0,1,1,""],File:[0,1,1,""],FileType:[0,1,1,""],StorageService:[0,1,1,""],get_mets_tasks:[0,1,1,""],package_tasks:[0,1,1,""]},"AIPscan.models.AIP":{create_date:[0,3,1,""],fetch_job_id:[0,3,1,""],files:[0,3,1,""],id:[0,3,1,""],original_file_count:[0,2,1,""],preservation_file_count:[0,2,1,""],storage_service_id:[0,3,1,""],transfer_name:[0,3,1,""],uuid:[0,3,1,""]},"AIPscan.models.Agent":{Event:[0,3,1,""],agent_type:[0,3,1,""],agent_value:[0,3,1,""],id:[0,3,1,""],linking_type_value:[0,3,1,""]},"AIPscan.models.Event":{date:[0,3,1,""],detail:[0,3,1,""],event_agents:[0,3,1,""],file:[0,3,1,""],file_id:[0,3,1,""],id:[0,3,1,""],outcome:[0,3,1,""],outcome_detail:[0,3,1,""],type:[0,3,1,""],uuid:[0,3,1,""]},"AIPscan.models.FetchJob":{aips:[0,3,1,""],download_directory:[0,3,1,""],download_end:[0,3,1,""],download_start:[0,3,1,""],id:[0,3,1,""],storage_service:[0,3,1,""],storage_service_id:[0,3,1,""],total_aips:[0,3,1,""],total_deleted_aips:[0,3,1,""],total_dips:[0,3,1,""],total_packages:[0,3,1,""],total_replicas:[0,3,1,""],total_sips:[0,3,1,""]},"AIPscan.models.File":{aip:[0,3,1,""],aip_id:[0,3,1,""],checksum_type:[0,3,1,""],checksum_value:[0,3,1,""],date_created:[0,3,1,""],derivatives:[0,3,1,""],events:[0,3,1,""],file_format:[0,3,1,""],file_type:[0,3,1,""],filepath:[0,3,1,""],format_version:[0,3,1,""],id:[0,3,1,""],name:[0,3,1,""],original_file:[0,3,1,""],original_file_id:[0,3,1,""],puid:[0,3,1,""],size:[0,3,1,""],uuid:[0,3,1,""]},"AIPscan.models.FileType":{original:[0,3,1,""],preservation:[0,3,1,""]},"AIPscan.models.StorageService":{"default":[0,3,1,""],api_key:[0,3,1,""],download_limit:[0,3,1,""],download_offset:[0,3,1,""],fetch_jobs:[0,3,1,""],id:[0,3,1,""],name:[0,3,1,""],url:[0,3,1,""],user_name:[0,3,1,""]},"AIPscan.models.get_mets_tasks":{get_mets_task_id:[0,3,1,""],package_uuid:[0,3,1,""],status:[0,3,1,""],workflow_coordinator_id:[0,3,1,""]},"AIPscan.models.package_tasks":{package_task_id:[0,3,1,""],workflow_coordinator_id:[0,3,1,""]},AIPscan:{API:[1,0,0,"-"],Aggregator:[2,0,0,"-"],Data:[4,0,0,"-"],Home:[6,0,0,"-"],Reporter:[7,0,0,"-"],User:[8,0,0,"-"],celery:[0,0,0,"-"],conftest:[0,0,0,"-"],create_app:[0,4,1,""],extensions:[0,0,0,"-"],helpers:[0,0,0,"-"],models:[0,0,0,"-"],worker:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"],"4":["py","function","Python function"],"5":["py","exception","Python exception"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute","4":"py:function","5":"py:exception"},terms:{"0000":2,"class":[0,1,2,8],"default":[0,1,2],"enum":0,"function":[0,1,2,3],"import":1,"return":[0,1,2,4,5,7],"true":[1,3,4],And:1,One:1,The:[0,2,9],There:2,abil:3,about:2,absolut:1,accept:4,across:[3,4,7],add:[0,1,2,3],addit:[0,9],agent:[0,2,3],agent_link_multipli:3,agent_typ:0,agent_valu:0,aggreg:[0,9,10],aid:2,aip:[0,1,2,3,4,7,9],aip_cont:7,aip_id:[0,7],aip_overview:4,aip_overview_two:4,aiplist:1,all:[1,2,3,4,7],alreadi:2,also:2,analysi:2,ani:[2,3],apart:2,api:[0,2,9,10],api_kei:[0,2],api_url:[2,3],apiurl:2,app:[0,9],app_inst:[0,3,5],applic:[0,1,6],arg:[1,2,8],around:7,artefactu:9,ask:2,associ:[2,3,7],avail:7,avoid:1,base:[0,1,2,8],base_url:3,below:9,better:2,between:[4,7],bia:1,blueprint:7,booleanfield:[2,8],both:7,built:7,call:2,caller:[1,2,7],can:[0,1,2,3,9],cannot:2,celeri:[2,9,10],celery_help:[0,9,10],chart:7,chart_formats_count:7,check:[2,3],checksum_typ:0,checksum_valu:0,clean:2,code:[0,7],collect:[2,3],collect_mets_ag:2,commun:1,complet:2,compon:[2,7],compressed_ext:2,config_nam:0,configur:0,configure_celeri:0,conform:5,conftest:[9,10],consist:7,construct:[2,3],constructor:3,contain:[0,2,4,7,9],content:[9,10],context:0,convert:9,coordinatorid:2,copi:[4,7],correct:2,count:[2,7],creat:[1,2,3,4],create_agent_object:2,create_aip_object:2,create_app:0,create_d:[0,2],create_event_object:2,create_numbered_subdir:2,creation:3,critic:2,current:9,current_path:[2,3],cut:1,data:[0,1,2,9,10],data_aip_list:[],data_derivative_list:[],data_fmt_list:[],data_largest_file_list:[],databas:[0,2,3,9],database_help:[0,9,10],datarequir:[2,8],date:[0,7],date_cr:0,datetim:3,declar:0,default_pair_tre:2,defin:[0,6],delet:2,delete_fetch_job:2,delete_storage_servic:2,demonstr:1,deprec:1,deriv:[0,4,7],derivative_overview:4,derivativelist:1,desc:4,describ:[7,9],descript:[],desir:[1,2],detail:[0,7],determin:2,develop:9,dict:4,differ:[1,7],digit:9,dip:2,directori:3,disk:[2,7],displai:7,document:2,don:2,download:2,download_directori:0,download_end:0,download_limit:[0,2],download_met:2,download_offset:[0,2],download_start:0,duplic:2,easi:2,edit_storage_servic:2,encount:2,endpoint:1,ensur:[2,3],entri:1,enumer:0,equal:3,equival:2,error:2,establish:3,evalu:3,event:[0,2,3,7],event_ag:0,event_agent_relationship:3,event_count:3,evolv:1,exampl:0,except:2,exist:[2,7],expect:[3,5],ext:0,extend:[0,1],extens:[9,10],factori:0,fetch_job:0,fetch_job_id:[0,2],fetchjob:0,fetchjobid:2,field:[1,4],file:[0,1,2,3,4,5,7],file_count:5,file_data:5,file_format:0,file_id:[0,2,7],file_typ:[0,1,4],filepath:0,filetyp:0,filter:[1,4,7],fixtur:0,fixture_path:3,flask:0,flask_restx:1,flask_wtf:[2,8],flaskform:[2,8],fmtlist:1,folder:2,follow:[1,4],form:[0,9,10],format:[2,4,7],format_api_url_with_limit_offset:2,format_vers:0,friendli:7,from:[0,1,2,3,7],fs_entri:2,fsentri:2,further:[2,3],get:[1,2,3],get_aip_original_nam:2,get_human_readable_file_s:0,get_met:2,get_mets_task:0,get_mets_task_id:0,get_mets_task_statu:2,get_mets_url:2,get_packages_directori:2,get_relative_path:2,given:[2,3,4,7],good:1,handl:[2,3,6],has_format_vers:5,has_puid:5,have:[2,7],haven:3,header:7,help:2,helper:[2,3,9,10],here:[0,2],home:[0,10],how:1,howto:1,http:2,http_respons:2,identifi:[],idx:3,improv:9,inc:9,index:[6,9],info:1,inform:[2,3,9],infos_vers:[],init:0,initi:[0,2,7],input_d:3,instanc:0,instead:1,interest:2,intervent:2,is_aip:2,is_delet:2,is_dip:2,is_replica:2,is_sip:2,itself:[1,7],json:2,json_file_path:2,just:1,kei:2,know:2,known:2,kwarg:[0,1,2,8],lack:0,largest:[1,4,7],largest_fil:[4,7],largestfilelist:1,let:2,like:1,limit:[1,2,4],linking_type_valu:0,list:[2,3,4,7],load:[0,2],logic:3,loginform:8,machin:7,make:[2,3],manag:9,map:[4,7],match:5,mediatyp:[],met:[2,3],metadata:[1,2,7],method:1,mets_error:3,mets_fil:2,mets_parse_help:[0,9,10],metsdocu:2,metserror:2,mocker:[3,5],model:[9,10],modul:[9,10],moment:1,more:7,multipl:9,name:[0,2,3,4],namespac:1,namespace_data:[0,9,10],namespace_info:[0,9,10],navig:7,necessari:1,need:[0,2,7,9],new_fetch_job:2,new_storage_servic:2,none:[0,1,4],notabl:7,now_year:3,number:[2,4],number_of_unique_ag:3,object:[2,3,7,8],off:7,offset:2,okai:3,onli:[1,3],option:4,order:4,organ:7,origin:[0,2,3,4,7],original_deriv:7,original_fil:[0,4],original_file_count:0,original_file_id:0,other:[2,3],our:[0,2,3],outcom:0,outcome_detail:0,output_d:3,outsid:7,over:2,overview:[1,4,7,9],packag:[9,10,11],package_list_no:2,package_list_numb:[2,3],package_lists_task:2,package_obj:2,package_task:0,package_task_id:0,package_uuid:[0,2,3],packageerror:2,packagelistno:2,packages_directori:2,packageuuid:2,page:[7,9],param:[],paramet:[2,4],pars:[2,3],parse_bool:1,parse_mets_with_metsrw:2,parse_packages_and_load_met:2,password:8,passwordfield:8,path:[2,3],path_to_met:[2,3],pattern:0,per:3,perform:2,pie:7,plan:9,plot:7,plot_formats_count:7,plu:2,point:[1,2],popul:2,possibl:2,potenti:3,precis:0,premi:[2,3],present:7,preserv:[0,4,7,9],preservation_file_count:0,primari:7,problem:2,process:2,process_aip_data:2,process_package_object:2,properli:[2,3],properti:0,propos:1,provid:[0,1,2,3,7],puid:0,pytest:0,queri:[3,4],rang:7,raw:1,reach:2,readabl:7,reader:[2,3],record:2,refactor:2,region:3,rel:[2,3],relat:0,relativepathtomet:2,reliabl:[2,3],remain:2,rememb:8,remember_m:8,remix:1,repeat:3,replica:2,report:[0,2,4,9,10],report_aip_cont:[0,9,10],report_formats_count:[0,9,10],report_largest_fil:[0,9,10],report_originals_with_deriv:[0,9,10],repositori:9,requir:[1,2],resourc:1,respons:[2,7],result:[2,3,4],retriev:[2,3,9],reusabl:2,rout:[6,7],run:0,save:2,scatter:7,search:9,see:3,sensibl:3,separ:[0,7],serial:9,servic:[0,2,3,4,7,9],set:2,share:[0,7],should:2,show:1,sign:8,signal:2,sinc:0,singular:7,sip:2,siphon:7,size:[0,4,7],some:[3,9],someth:7,sound:3,sourc:[0,1,2,3,4,5,6,7,8],specif:[3,7],sqlalchemi:0,ss_default:2,stakehold:1,standard:7,start_mets_task:2,state:0,statu:[0,2],storag:[2,3,4,7,9],storage_servic:[0,2],storage_service_id:[0,1,2,4,7],storage_service_packag:3,storagenam:4,storageservic:0,storageserviceform:2,storageserviceid:2,storageservicepackag:2,store:[2,3,9],stream:2,string:[],stringfield:[2,8],structur:5,sub:3,subdir:2,submit:8,submitfield:8,submodul:[9,10],subpackag:[9,10],summari:4,sunset:1,support:9,sure:[2,3],swagger:1,system:9,systemd:0,tabular:7,task:[0,9,10],task_help:[0,9,10],task_statu:2,taskerror:2,taskid:2,teas:2,test:[0,2,4,10],test_collect_ag:3,test_create_aip:3,test_create_numbered_subdir:3,test_database_help:[0,2,10],test_event_cr:3,test_fil:5,test_format_api_url:3,test_get_aip_original_nam:3,test_get_mets_url:3,test_get_relative_path:3,test_largest_fil:[0,4,10],test_largest_files_el:5,test_met:[0,2,10],test_package_:3,test_process_package_object:3,test_storage_service_package_eq:3,test_storage_service_package_init:3,test_task_help:[0,2,10],test_typ:[0,2,10],test_tz_neutral_d:3,thei:[3,7,9],themselv:7,thi:[0,1,2,9,11],thing:2,time:2,timestamp:[2,3],timestampstr:2,too:[3,7],total_aip:0,total_deleted_aip:0,total_dip:0,total_packag:0,total_replica:0,total_sip:0,transfer_nam:[0,2,3],translat:7,translate_head:7,two:1,type:[0,3,4,9,10],unboundfield:[2,8],uniqu:[2,3],unique_ag:2,unkempt:1,unless:2,upper:4,url:[0,2,3],url_api_dict:3,url_with_api_kei:3,url_without_api_kei:3,use:[0,1,2,3],used:1,user:[0,2,7,10],user_nam:[0,2],usernam:8,using:0,util:9,uuid:0,val:1,valid:[2,8],valu:[0,3,4,5],version:1,via:0,view:[0,9,10],view_aip:7,view_fil:7,want:2,web:9,well:1,when:[1,2,3],whether:2,which:[0,2,7,9],within:3,without:1,work:[3,9],worker:[2,9,10],workflow_coordin:2,workflow_coordinator_id:0,write:2,write_celery_upd:2,write_packages_json:2,writer:[2,3],written:[2,3],wtform:[2,8]},titles:["AIPscan package","AIPscan.API package","AIPscan.Aggregator package","AIPscan.Aggregator.tests package","AIPscan.Data package","AIPscan.Data.tests package","AIPscan.Home package","AIPscan.Reporter package","AIPscan.User package","Welcome to AIPscan\u2019s documentation!","AIPscan","Overview"],titleterms:{aggreg:[2,3],aipscan:[0,1,2,3,4,5,6,7,8,9,10],api:1,celeri:0,celery_help:2,conftest:0,content:[0,1,2,3,4,5,6,7,8],data:[4,5],database_help:2,document:9,extens:0,form:[2,8],helper:[0,7],home:6,indic:9,mets_parse_help:2,model:0,modul:[0,1,2,3,4,5,6,7,8],namespace_data:1,namespace_info:1,overview:11,packag:[0,1,2,3,4,5,6,7,8],report:7,report_aip_cont:7,report_formats_count:7,report_largest_fil:7,report_originals_with_deriv:7,submodul:[0,1,2,3,4,5,6,7,8],subpackag:[0,2,4],tabl:9,task:2,task_help:2,test:[3,5],test_database_help:3,test_largest_fil:5,test_met:3,test_task_help:3,test_typ:3,type:2,user:8,view:[1,2,6,7,8],welcom:9,worker:0}}) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..7d1a1afe --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,30 @@ +.. AIPscan documentation master file, created by + sphinx-quickstart on Fri Nov 13 14:04:02 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to AIPscan's documentation! +=================================== + +AIPscan is a utility developed by Artefactual Systems Inc. The utility +is currently a web-app that can retrieve the contents of the AIPs +stored in multiple storage services to then convert the information +they contain into a database serialization which can support repository +management and digital preservation planning. + +The contents of this package are described below. Some additional work +is needed to improve this! + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + + overview + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..2119f510 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 00000000..e1486566 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,11 @@ +AIPscan +======= + +.. toctree:: + :maxdepth: 4 + + AIPscan + AIPscan.API + AIPscan.Data + AIPscan.Aggregator + AIPscan.Reporter diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 00000000..16e6296e --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,4 @@ +Overview +======== + +This is an overview of the package. From 4efcd83c9fed9a6de57f9ec532121d99b444e914 Mon Sep 17 00:00:00 2001 From: Ross Spencer Date: Fri, 13 Nov 2020 18:13:15 -0500 Subject: [PATCH 18/18] Add dev requirements NB. This is a hammer-nail solution, we'll want to take this a different direction, e.g. inheriting from base requirements first, and adding dev reqs on-top, before merging. --- requirements/dev.txt | 173 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 requirements/dev.txt diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 00000000..828fc95f --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,173 @@ +alabaster==0.7.11 +amclient==1.0.0rc2 +ammcpc==0.1.3 +args==0.1.0 +aspy.yaml==1.1.1 +astroid==1.6.5 +atomicwrites==1.3.0 +attrs==19.3.0 +awscli==1.16.205 +Babel==2.6.0 +backports-abc==0.5 +backports.csv==1.0.6 +backports.functools-lru-cache==1.5 +backports.ssl-match-hostname==3.5.0.1 +bagit==1.7.0 +bcrypt==3.1.6 +beautifulsoup4==4.8.2 +behave==1.2.6 +bleach==3.1.0 +botocore==1.12.195 +cached-property==1.5.1 +certifi==2020.6.20 +cffi==1.12.2 +cfgv==1.1.0 +chardet==3.0.4 +clamd==1.0.2 +Click==7.0 +clint==0.5.1 +colorama==0.3.9 +configparser==3.5.0 +contextlib2==0.5.5 +coverage==4.5.3 +cycler==0.10.0 +Django==1.11.29 +django-autoslug==1.9.7 +django-extensions==1.7.9 +django-tastypie==0.13.2 +dlib==19.19.0 +docker==3.7.3 +docker-compose==1.24.1 +docker-pycreds==0.4.0 +dockerpty==0.4.1 +docopt==0.6.2 +docutils==0.14 +elasticsearch==6.3.1 +enum34==1.1.6 +face-recognition==1.3.0 +face-recognition-models==0.3.0 +filelock==3.0.12 +fixity==0.3 +flake8==3.4.1 +flake8-import-order==0.13 +funcsigs==1.0.2 +functools32==3.2.3.post2 +future==0.18.2 +futures==3.2.0 +gearman==2.0.2 +html5lib==1.0.1 +identify==1.1.7 +idna==2.8 +imagesize==1.0.0 +importlib-metadata==0.18 +importlib-resources==1.0.2 +internetarchive==1.8.1 +ipaddress==1.0.22 +isodate==0.6.0 +isort==4.3.4 +Jinja2==2.10 +jmespath==0.9.4 +jpylyzer==1.18.0 +jsonpatch==1.23 +jsonpointer==2.0 +jsonschema==2.6.0 +kiwisolver==1.0.1 +lazy-object-proxy==1.3.1 +linecache2==1.0.0 +livereload==2.6.0 +lxml==3.5.0 +Markdown==3.1 +MarkupSafe==1.0 +matplotlib==2.2.3 +mccabe==0.6.1 +metsrw==0.3.15 +mkdocs==1.0.4 +mock==3.0.5 +more-itertools==5.0.0 +MySQL-python==1.2.5 +mysqlclient==1.4.6 +nltk==3.4 +nodeenv==1.3.3 +numpy==1.16.5 +oauthlib==3.0.1 +olefile==0.46 +opencv-python==4.2.0.32 +opf-fido==1.4.1 +packaging==17.1 +pandas==0.24.2 +paramiko==2.4.2 +parse==1.8.4 +parse-type==0.4.2 +pathlib2==2.3.3 +pbr==5.4.5 +Pillow==5.2.0 +pip-tools==3.8.0 +pkginfo==1.5.0.1 +plotly==4.5.0 +pluggy==0.12.0 +plyvel==1.0.5 +pre-commit==1.12.0 +pretty-cron==1.2.0 +prometheus-client==0.7.1 +py==1.8.1 +pycodestyle==2.3.1 +pycparser==2.19 +pydoc-markdown==2.0.5 +pyflakes==1.5.0 +Pygments==2.2.0 +PyGObject==3.36.0 +pylint==1.9.5 +PyNaCl==1.3.0 +pyparsing==2.2.0 +pypi==2.1 +pytest==4.6.3 +pytest-cov==2.7.1 +pytest-mock==1.10.0 +python-dateutil==2.8.1 +python-mimeparse==1.6.0 +pytidylib==0.3.2 +pytz==2020.1 +PyYAML==3.13 +rdflib==4.2.2 +readme-renderer==24.0 +requests==2.21.0 +requests-oauthlib==1.2.0 +requests-toolbelt==0.9.1 +requirements==0.1 +retrying==1.3.3 +rsa==3.4.2 +s3transfer==0.2.1 +scandir==1.10.0 +schema==0.6.8 +scipy==1.1.0 +seaborn==0.9.0 +selenium==3.14.0 +singledispatch==3.4.0.3 +six==1.15.0 +snowballstemmer==1.2.1 +soupsieve==1.9.5 +SPARQLWrapper==1.8.2 +specktre==0.2.0 +sphinxcontrib-websupport==1.1.0 +SQLAlchemy==1.3.4 +subprocess32==3.5.3 +texttable==0.9.1 +toml==0.9.6 +tornado==5.1.1 +tox==3.12.1 +tqdm==4.28.1 +traceback2==1.4.0 +twarc==1.6.3 +twine==1.15.0 +typing==3.6.4 +unicodecsv==0.14.1 +Unidecode==0.4.19 +urllib3==1.24.3 +vboxapi==1.0 +vcrpy==2.0.1 +virtualenv==16.0.0 +wcwidth==0.1.7 +webencodings==0.5.1 +websocket-client==0.54.0 +wrapt==1.10.11 +zipp==0.5.1