From 42daa9415ead862ced45d04c5ea760c1cce530e1 Mon Sep 17 00:00:00 2001 From: Jin-Sun-tts Date: Thu, 28 Mar 2024 13:19:17 -0400 Subject: [PATCH 1/3] added routes --- app/__init__.py | 2 + app/flask-app-structure.txt | 22 -- app/forms.py | 26 ++ app/interface.py | 174 ++++++++++--- app/models.py | 39 +-- app/readme.txt | 117 +++++++++ app/routes.py | 231 +++++++++++++----- app/templates/harvest_source.html | 45 ++++ app/templates/index.html | 55 ++++- app/templates/org_form.html | 16 ++ app/templates/source_form.html | 21 ++ docker-compose.yml | 4 +- ...cbc2f2_base_models.py => 19ffcfff6080_.py} | 55 ++--- requirements.txt | 1 + tests/database/test_db.py | 4 +- 15 files changed, 632 insertions(+), 180 deletions(-) delete mode 100644 app/flask-app-structure.txt create mode 100644 app/forms.py create mode 100644 app/readme.txt create mode 100644 app/templates/harvest_source.html create mode 100644 app/templates/org_form.html create mode 100644 app/templates/source_form.html rename migrations/versions/{701baacbc2f2_base_models.py => 19ffcfff6080_.py} (78%) diff --git a/app/__init__.py b/app/__init__.py index fe306ee1..d7884387 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -11,6 +11,8 @@ def create_app(): app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URI + SECRET_KEY = os.urandom(16) + app.config['SECRET_KEY'] = SECRET_KEY db.init_app(app) # Initialize Flask-Migrate diff --git a/app/flask-app-structure.txt b/app/flask-app-structure.txt deleted file mode 100644 index 26f38763..00000000 --- a/app/flask-app-structure.txt +++ /dev/null @@ -1,22 +0,0 @@ -DATAGOV-HARVESTING-LOGIC -├── app/ -│ ├── __init__.py -│ ├── models.py -│ ├── routes.py -│ ├── forms.py (to-do) -│ └── templates/ -│ ├── index.html -│ ├── harvest_source_form.html (to-do) -│ └── xxx.html (to-do) -│ └── static/ -│ └── styles.css (to-do) -│ -├── migrations/ -│ └── versions/ -│ ├── alembic.ini -│ ├── env.py -│ └── script.py.mako -│ -├── docker-compose.yml -├── Dockerfile -└── run.py diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 00000000..d444a328 --- /dev/null +++ b/app/forms.py @@ -0,0 +1,26 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField +from wtforms.validators import DataRequired, URL, ValidationError +import re + +def validate_email_list(form, field): + emails = field.data.split(',') + for email in emails: + if not re.match(r"[^@]+@[^@]+\.[^@]+", email.strip()): + raise ValidationError("Invalid email address: {}".format(email)) + +class HarvestSourceForm(FlaskForm): + # organization_id = StringField('organization_id', validators=[DataRequired()]) + organization_id = SelectField('Organization', choices=[], validators=[DataRequired()]) + name = StringField('Name', validators=[DataRequired()]) + url = StringField('URL', validators=[DataRequired(), URL()]) + emails = StringField('Notification_emails', validators=[DataRequired(), validate_email_list]) + frequency = SelectField('Frequency', choices=['daily', 'monthly', 'yearly'], validators=[DataRequired()]) + schema_type = StringField('Schema Type', validators=[DataRequired()]) + source_type = StringField('Source Type', validators=[DataRequired()]) + submit = SubmitField('Submit') + +class OrganizationForm(FlaskForm): + name = StringField('Name', validators=[DataRequired()]) + logo = StringField('Logo', validators=[DataRequired()]) + submit = SubmitField('Submit') \ No newline at end of file diff --git a/app/interface.py b/app/interface.py index ed3f91a8..a1b95d35 100644 --- a/app/interface.py +++ b/app/interface.py @@ -1,5 +1,6 @@ from sqlalchemy import create_engine, inspect from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.exc import NoResultFound, IntegrityError from app.models import Organization, HarvestSource, HarvestJob, HarvestError from . import DATABASE_URI @@ -20,25 +21,63 @@ def _to_dict(obj): for c in inspect(obj).mapper.column_attrs} def add_organization(self, org_data): - new_org = Organization(**org_data) - self.db.add(new_org) - self.db.commit() - self.db.refresh(new_org) - return new_org - - def add_harvest_source(self, source_data, org_id): - source_data['organization_id'] = org_id - new_source = HarvestSource(**source_data) - self.db.add(new_source) - self.db.commit() - self.db.refresh(new_source) - return new_source + try: + new_org = Organization(**org_data) + self.db.add(new_org) + self.db.commit() + self.db.refresh(new_org) + return new_org + except Exception as e: + print("Error:", e) + self.db.rollback() + return None def get_all_organizations(self): orgs = self.db.query(Organization).all() orgs_data = [ HarvesterDBInterface._to_dict(org) for org in orgs] return orgs_data + + def get_organization(self, org_id): + result = self.db.query(Organization).filter_by(id=org_id).first() + return HarvesterDBInterface._to_dict(result) + + def update_organization(self, org_id, updates): + try: + org = self.db.query(Organization).get(org_id) + + for key, value in updates.items(): + if hasattr(org, key): + setattr(org, key, value) + else: + print(f"Warning: Trying to update non-existing field '{key}' in organization") + + self.db.commit() + return self._to_dict(org) + + except NoResultFound: + self.db.rollback() + return None + + def delete_organization(self, org_id): + org = self.db.query(Organization).get(org_id) + if org is None: + return "Organization not found" + self.db.delete(org) + self.db.commit() + return "Organization deleted successfully" + + def add_harvest_source(self, source_data): + try: + new_source = HarvestSource(**source_data) + self.db.add(new_source) + self.db.commit() + self.db.refresh(new_source) + return new_source + except Exception as e: + print("Error:", e) + self.db.rollback() + return None def get_all_harvest_sources(self): harvest_sources = self.db.query(HarvestSource).all() @@ -49,14 +88,50 @@ def get_all_harvest_sources(self): def get_harvest_source(self, source_id): result = self.db.query(HarvestSource).filter_by(id=source_id).first() return HarvesterDBInterface._to_dict(result) + + def get_harvest_source_by_org(self, org_id): + harvest_source = self.db.query( + HarvestSource).filter_by(organization_id=org_id).all() + harvest_source_data = [ + HarvesterDBInterface._to_dict(src) for src in harvest_source] + return harvest_source_data - def add_harvest_job(self, job_data, source_id): - job_data['harvest_source_id'] = source_id - new_job = HarvestJob(**job_data) - self.db.add(new_job) + def update_harvest_source(self, source_id, updates): + try: + source = self.db.query(HarvestSource).get(source_id) + + for key, value in updates.items(): + if hasattr(source, key): + setattr(source, key, value) + else: + print(f"Warning: Trying to update non-existing field '{key}' in HarvestSource") + + self.db.commit() + return self._to_dict(source) + + except NoResultFound: + self.db.rollback() + return None + + def delete_harvest_source(self, source_id): + source = self.db.query(HarvestSource).get(source_id) + if source is None: + return "Harvest source not found" + self.db.delete(source) self.db.commit() - self.db.refresh(new_job) - return new_job + return "Harvest source deleted successfully" + + def add_harvest_job(self, job_data): + try: + new_job = HarvestJob(**job_data) + self.db.add(new_job) + self.db.commit() + self.db.refresh(new_job) + return new_job + except Exception as e: + print("Error:", e) + self.db.rollback() + return None def get_all_harvest_jobs(self): harvest_jobs = self.db.query(HarvestJob).all() @@ -67,16 +142,59 @@ def get_all_harvest_jobs(self): def get_harvest_job(self, job_id): result = self.db.query(HarvestJob).filter_by(id=job_id).first() return HarvesterDBInterface._to_dict(result) + + def get_harvest_job_by_source(self, source_id): + harvest_job = self.db.query( + HarvestJob).filter_by(harvest_source_id=source_id).all() + harvest_job_data = [ + HarvesterDBInterface._to_dict(job) for job in harvest_job] + return harvest_job_data - def add_harvest_error(self, error_data, job_id): - error_data['harvest_job_id'] = job_id - new_error = HarvestError(**error_data) - self.db.add(new_error) + def update_harvest_job(self, job_id, updates): + try: + job = self.db.query(HarvestJob).get(job_id) + + for key, value in updates.items(): + if hasattr(job, key): + setattr(job, key, value) + else: + print(f"Warning: Trying to update non-existing field '{key}' in HavestJob") + + self.db.commit() + return self._to_dict(job) + + except NoResultFound: + self.db.rollback() + return None + + def delete_harvest_job(self, job_id): + job = self.db.query(HarvestJob).get(job_id) + if job is None: + return "Harvest job not found" + self.db.delete(job) self.db.commit() - self.db.refresh(new_error) - return new_error + return "Harvest job deleted successfully" + + def add_harvest_error(self, error_data): + try: + new_error = HarvestError(**error_data) + self.db.add(new_error) + self.db.commit() + self.db.refresh(new_error) + return new_error + except Exception as e: + print("Error:", e) + self.db.rollback() + return None + + # for test, will remove later + def get_all_harvest_errors(self): + harvest_errors = self.db.query(HarvestError).all() + harvest_errors_data = [ + HarvesterDBInterface._to_dict(err) for err in harvest_errors] + return harvest_errors_data - def get_all_harvest_errors_by_job(self, job_id): + def get_harvest_error_by_job(self, job_id): harvest_errors = self.db.query(HarvestError).filter_by(harvest_job_id=job_id) harvest_errors_data = [ HarvesterDBInterface._to_dict(err) for err in harvest_errors] @@ -87,7 +205,7 @@ def get_harvest_error(self, error_id): return HarvesterDBInterface._to_dict(result) def close(self): - if hasattr(self.db, 'remove'): + if hasattr(self.db, "remove"): self.db.remove() - elif hasattr(self.db, 'close'): + elif hasattr(self.db, "close"): self.db.close() \ No newline at end of file diff --git a/app/models.py b/app/models.py index 4c242363..46672a09 100644 --- a/app/models.py +++ b/app/models.py @@ -12,32 +12,37 @@ class Base(db.Model): server_default=text("gen_random_uuid()")) class Organization(Base): - __tablename__ = 'organization' + __tablename__ = "organization" name = db.Column(db.String(), nullable=False, index=True) logo = db.Column(db.String()) + sources = db.relationship("HarvestSource", backref="org", + cascade="all, delete-orphan", + lazy=True) class HarvestSource(Base): - __tablename__ = 'harvest_source' + __tablename__ = "harvest_source" name = db.Column(db.String, nullable=False) - notification_emails = db.Column(ARRAY(db.String)) + notification_emails = db.Column(db.String) organization_id = db.Column(UUID(as_uuid=True), - db.ForeignKey('organization.id'), + db.ForeignKey("organization.id"), nullable=False) frequency = db.Column(db.String, nullable=False) url = db.Column(db.String, nullable=False, unique=True) schema_type = db.Column(db.String, nullable=False) source_type = db.Column(db.String, nullable=False) - jobs = db.relationship('HarvestJob', backref='source') + jobs = db.relationship("HarvestJob", backref="source", + cascade="all, delete-orphan", + lazy=True) class HarvestJob(Base): - __tablename__ = 'harvest_job' + __tablename__ = "harvest_job" harvest_source_id = db.Column(UUID(as_uuid=True), - db.ForeignKey('harvest_source.id'), + db.ForeignKey("harvest_source.id"), nullable=False) - status = db.Column(Enum('new', 'in_progress', 'complete', name='job_status'), + status = db.Column(Enum("new", "in_progress", "complete", name="job_status"), nullable=False, index=True) date_created = db.Column(db.DateTime, index=True) @@ -47,36 +52,38 @@ class HarvestJob(Base): records_deleted = db.Column(db.Integer) records_errored = db.Column(db.Integer) records_ignored = db.Column(db.Integer) - errors = db.relationship('HarvestError', backref='job', lazy=True) + errors = db.relationship("HarvestError", backref="job", + cascade="all, delete-orphan", + lazy=True) class HarvestError(Base): - __tablename__ = 'harvest_error' + __tablename__ = "harvest_error" harvest_job_id = db.Column(UUID(as_uuid=True), - db.ForeignKey('harvest_job.id'), + db.ForeignKey("harvest_job.id"), nullable=False) harvest_record_id = db.Column(db.String) # to-do # harvest_record_id = db.Column(UUID(as_uuid=True), - # db.ForeignKey('harvest_record.id'), + # db.ForeignKey("harvest_record.id"), # nullable=True) date_created = db.Column(db.DateTime) type = db.Column(db.String) - severity = db.Column(Enum('CRITICAL', 'ERROR', 'WARN', name='error_serverity'), + severity = db.Column(Enum("CRITICAL", "ERROR", "WARN", name="error_serverity"), nullable=False, index=True) message = db.Column(db.String) class HarvestRecord(Base): - __tablename__ = 'harvest_record' + __tablename__ = "harvest_record" job_id = db.Column(UUID(as_uuid=True), - db.ForeignKey('harvest_job.id'), + db.ForeignKey("harvest_job.id"), nullable=False) identifier = db.Column(db.String(), nullable=False) ckan_id = db.Column(db.String(), nullable=False, index=True) type = db.Column(db.String(), nullable=False) source_metadata = db.Column(db.String(), nullable=True) __table_args__ = ( - UniqueConstraint('job_id', 'identifier', name='uix_job_id_identifier'), + UniqueConstraint("job_id", "identifier", name="uix_job_id_identifier"), ) \ No newline at end of file diff --git a/app/readme.txt b/app/readme.txt new file mode 100644 index 00000000..8ec993b7 --- /dev/null +++ b/app/readme.txt @@ -0,0 +1,117 @@ +DATAGOV-HARVESTING-LOGIC +├── app/ +│ ├── __init__.py +│ ├── models.py +│ ├── interface.py +│ ├── routes.py +│ ├── forms.py +│ └── templates/ +│ ├── index.html +│ ├── source_form.html +│ ├── org_form.html +│ └── harvest_source.html +│ └── static/ +│ └── styles.css (to-do) +│ +├── migrations/ +│ └── versions/ +│ ├── alembic.ini +│ ├── env.py +│ └── script.py.mako +│ +├── tests/ +│ +├── docker-compose.yml +├── Dockerfile +├── .profile +├── requirements.txt +└── run.py + + + +curl -X POST http://localhost:8080/organization/add -H "Content-Type: application/json" -d ' +{ + "name": "New Org", + "logo": "test url" +}' + +curl -X POST http://localhost:8080/harvest_source/add -H "Content-Type: application/json" -d ' +{ + "organization_id": "c32e18ee-a854-47be-a05f-42516498a44d", + "name": "Example Harvest Source", + "notification_emails": "admin@example.com", + "frequency": "daily", + "url": "http://example2.com", + "schema_type": "strict", + "source_type": "json" +} +' + +curl -X POST http://localhost:8080/harvest_job/add -H "Content-Type: application/json" -d ' +{ + "harvest_source_id": "760129d6-2bf0-4c94-94b9-09622a8a0b23", + "status": "in_progress", + "date_created": "Wed, 27 Mar 2024 20:37:52 GMT" +}' + + +curl -X POST http://localhost:8080/harvest_error/add -H "Content-Type: application/json" -d ' +{ + "harvest_job_id": "aac30640-bd76-46c2-8a64-cf8ee389b190", + "harvest_record_id": "record123", + "date_created": "Wed, 27 Mar 2024 20:37:52 GMT", + "type": "Validation Error", + "severity": "ERROR", + "message": "Invalid data format." +} +' + + +curl -X DELETE http://localhost:8080/organization/da183992-e598-467a-b245-a3fe8ee2fb91 + +curl -X DELETE http://localhost:8080/harvest_source/c7abedad-4420-4d71-b519-3284f9a9a132 + + +curl -X PUT http://localhost:8080/harvest_job/c82e0481-5884-4029-931e-234c53767e50 -H "Content-Type: application/json" -d ' +{ + "status": "complete", + "date_finished": "Wed, 27 Mar 2024 22:37:52 GMT", + "records_added": 200, + "records_updated": 50, + "records_deleted": 6, + "records_errored": 4, + "records_ignored": 2 +}' + +------------- + +curl -X POST https://harvester-dev-datagov.app.cloud.gov/organization/add -H "Content-Type: application/json" -d ' +{ + "name": "New Org 1", + "logo": "test url for new org1" +}' + + +curl -X POST https://lharvester-dev-datagov.app.cloud.gov/harvest_job/add -H "Content-Type: application/json" -d ' +{ + "harvest_source_id": "760129d6-2bf0-4c94-94b9-09622a8a0b23", + "status": "in_progress", + "date_created": "Wed, 27 Mar 2024 20:37:52 GMT" +}' + +curl -X PUT https://harvester-dev-datagov.app.cloud.gov/harvest_job/add -H "Content-Type: application/json" -d ' +{ + "status": "complete", + "date_finished": "Wed, 27 Mar 2024 22:37:52 GMT", + "records_added": 200, + "records_updated": 50, + "records_deleted": 6, + "records_errored": 4, + "records_ignored": 2 +}' + + +curl -X PUT https://harvester-dev-datagov.app.cloud.gov/organization/4c456ed3-4717-4933-82c9-d87464063f19 -H "Content-Type: application/json" -d ' +{ + "logo": "url for test 1" +}' \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 648e505c..8832d06b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,92 +1,195 @@ -from flask import Blueprint, request, render_template +from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify from .interface import HarvesterDBInterface from tests.database.data import new_org, new_source, new_job, new_error +from .forms import HarvestSourceForm, OrganizationForm -mod = Blueprint('harvest', __name__) +mod = Blueprint("harvest", __name__) db = HarvesterDBInterface() -@mod.route('/', methods=['GET']) +@mod.route("/", methods=["GET"]) def index(): - return render_template('index.html') + return render_template("index.html") -@mod.route('/add_org', methods=['POST', 'GET']) +@mod.route("/organization/add", methods=["POST", "GET"]) def add_organization(): - org=db.add_organization(new_org) - return(f"Added new organization with ID: {org.id}") - -@mod.route('/add_source', methods=['POST', 'GET']) -def add_harvest_source(): - org_id = request.args.get('org_id', None) - if org_id is None: - return 'Please provide org_id: /add_source?org_id=xxx' + form = OrganizationForm() + if request.is_json: + org = db.add_organization(request.json) + if org: + return jsonify({"message": f"Added new organization with ID: {org.id}"}) + else: + return jsonify({"error": "Failed to add organization."}), 400 else: - source=db.add_harvest_source(new_source, org_id) - return(f"Added new source with ID: {source.id}") + form = OrganizationForm() + if form.validate_on_submit(): + new_org = { + "name": form.name.data, + "logo": form.logo.data + } + org=db.add_organization(new_org) + if org: + return f"Added new organization with ID: {org.id}" + else: + return "Failed to add organization." + return render_template("org_form.html", form=form) -@mod.route('/add_job', methods=['POST', 'GET']) -def add_harvest_job(): - source_id = request.args.get('source_id', None) - if source_id is None: - return 'Please provide source_id: /add_job?source_id=xxx' +@mod.route("/organization", methods=["GET"]) +@mod.route("/organization/", methods=["GET"]) +def get_organization(org_id=None): + if org_id: + org = db.get_organization(org_id) + return jsonify(org) if org else ("Not Found", 404) else: - job=db.add_harvest_job(new_job, source_id) - return(f"Added new job with ID: {job.id}") + org = db.get_all_organizations() + return org -@mod.route('/add_error', methods=['POST', 'GET']) -def add_harvest_error(): - job_id = request.args.get('job_id', None) - if job_id is None: - return 'Please provide job_id: /add_error?job_id=xxx' - else: - err=db.add_harvest_error(new_error, job_id) - return(f"Added new error with ID: {err.id}") +@mod.route("/organization/", methods=["PUT"]) +def update_organization(org_id): + result = db.update_organization(org_id, request.json) + return result -@mod.route('/organizations', methods=['GET']) -def get_all_organizations(): - result = db.get_all_organizations() +@mod.route("/organization/", methods=["DELETE"]) +def delete_organization(org_id): + result = db.delete_organization(org_id) return result - -@mod.route('/harvest_sources', methods=['GET']) + +@mod.route("/harvest_source/add", methods=["POST", "GET"]) +def add_harvest_source(): + form = HarvestSourceForm() + organizations = db.get_all_organizations() + organization_choices = [(str(org["id"]), f'{org["name"]} - {org["id"]}') + for org in organizations] + form.organization_id.choices = organization_choices + + if request.is_json: + org = db.add_harvest_source(request.json) + if org: + return jsonify({"message": f"Added new harvest source with ID: {org.id}"}) + else: + return jsonify({"error": "Failed to add harvest source."}), 400 + else: + if form.validate_on_submit(): + new_source = { + "name": form.name.data, + "notification_emails": form.emails.data, + "frequency": form.frequency.data, + "url": form.url.data, + "schema_type": form.schema_type.data, + "source_type": form.source_type.data, + "organization_id": form.organization_id.data + } + source=db.add_harvest_source(new_source) + if source: + return f"Added new source with ID: {source.id}" + else: + return "Failed to add harvest source." + return render_template("source_form.html", form=form, choices=organization_choices) + +# test interface, will remove later +@mod.route("/get_harvest_source", methods=["GET"]) def get_all_harvest_sources(): - result = db.get_all_harvest_sources() + source = db.get_all_harvest_sources() + org = db.get_all_organizations() + return render_template("harvest_source.html", sources=source, organizations=org) + +@mod.route("/harvest_source/", methods=["GET"]) +@mod.route("/harvest_source/", methods=["GET"]) +def get_harvest_source(source_id=None): + if source_id: + source = db.get_harvest_source(source_id) + return jsonify(source) if source else ("Not Found", 404) + + organization_id = request.args.get("organization_id") + if organization_id: + source = db.get_harvest_source_by_org(organization_id) + if not source: + return "No harvest sources found for this organization", 404 + else: + source = db.get_all_harvest_sources() + return jsonify(source) + +@mod.route("/harvest_source/", methods=["PUT"]) +def update_harvest_source(source_id): + result = db.update_harvest_source(source_id, request.json) return result -@mod.route('/harvest_jobs', methods=['GET']) -def get_all_harvest_jobs(): - result = db.get_all_harvest_jobs() +@mod.route("/harvest_source/", methods=["DELETE"]) +def delete_harvest_source(source_id): + result = db.delete_harvest_source(source_id) return result -@mod.route('/harvest_errors_by_job/', methods=['GET']) -def get_all_harvest_errors_by_job(job_id): - try: - result = db.get_all_harvest_errors_by_job(job_id) - return result - except Exception: - return " provide job_id" - -@mod.route('/harvest_source/', methods=['GET']) -def get_harvest_source(source_id): +@mod.route("/harvest_job/add", methods=["POST"]) +def add_harvest_job(): + if request.is_json: + job = db.add_harvest_job(request.json) + if job: + return jsonify({"message": f"Added new harvest job with ID: {job.id}"}) + else: + return jsonify({"error": "Failed to add harvest job."}), 400 + else: + return jsonify({"Please provide harvest job with json format."}) + +@mod.route("/harvest_job/", methods=["GET"]) +@mod.route("/harvest_job/", methods=["GET"]) +def get_harvest_job(job_id=None): try: - result = db.get_harvest_source(source_id) - return result - except Exception: - return " provide source_id" + if job_id: + job = db.get_harvest_job(job_id) + return jsonify(job) if job else ("Not Found", 404) + + source_id = request.args.get("harvest_source_id") + if source_id: + job = db.get_harvest_job_by_source(source_id) + if not job: + return "No harvest jobs found for this harvest source", 404 + else: + job = db.get_all_harvest_jobs() -@mod.route('/harvest_job/', methods=['GET']) -def get_harvest_job(job_id): - try: - result = db.get_harvest_job(job_id) - return result + return jsonify(job) except Exception: - return "provide job_id" + return "Please provide correct job_id or harvest_source_id" + +@mod.route("/harvest_job/", methods=["PUT"]) +def update_harvest_job(job_id): + result = db.update_harvest_job(job_id, request.json) + return result + +@mod.route("/harvest_job/", methods=["DELETE"]) +def delete_harvest_job(job_id): + result = db.delete_harvest_job(job_id) + return result + +@mod.route("/harvest_error/add", methods=["POST", "GET"]) +def add_harvest_error(): + if request.is_json: + error = db.add_harvest_error(request.json) + if error: + return jsonify({"message": f"Added new harvest error with ID: {error.id}"}) + else: + return jsonify({"error": "Failed to add harvest error."}), 400 + else: + return jsonify({"Please provide harvest error with json format."}) -@mod.route('/harvest_error/', methods=['GET']) -def get_harvest_error(error_id): +@mod.route("/harvest_error/", methods=["GET"]) +@mod.route("/harvest_error/", methods=["GET"]) +def get_harvest_error(error_id=None): try: - result = db.get_harvest_error(error_id) - return result + if error_id: + error = db.get_harvest_error(error_id) + return jsonify(error) if error else ("Not Found", 404) + + job_id = request.args.get("harvest_job_id") + if job_id: + error = db.get_harvest_error_by_job(job_id) + if not error: + return "No harvest errors found for this harvest job", 404 + else: + # for test, will remove later + error = db.get_all_harvest_errors() + + return jsonify(error) except Exception: - return "provide error_id" + return "Please provide correct error_id or harvest_job_id" def register_routes(app): app.register_blueprint(mod) \ No newline at end of file diff --git a/app/templates/harvest_source.html b/app/templates/harvest_source.html new file mode 100644 index 00000000..43eced17 --- /dev/null +++ b/app/templates/harvest_source.html @@ -0,0 +1,45 @@ + + + + + Harvest Sources + + +

Harvest Sources

+

+ +
+ + + + + Get Source + + + +
+ +

+ +
+ + + +
+ + + diff --git a/app/templates/index.html b/app/templates/index.html index f3114184..4e70201e 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -22,17 +22,50 @@

Harvest Actions

+ diff --git a/app/templates/org_form.html b/app/templates/org_form.html new file mode 100644 index 00000000..f2b194ca --- /dev/null +++ b/app/templates/org_form.html @@ -0,0 +1,16 @@ + + + + + Add Organization + + +

Add Organization

+
+ {{ form.hidden_tag() }} + {{ form.name.label }} {{ form.name() }}

+ {{ form.logo.label }} {{ form.logo() }}

+ {{ form.submit() }} +
+ + diff --git a/app/templates/source_form.html b/app/templates/source_form.html new file mode 100644 index 00000000..ea2d1204 --- /dev/null +++ b/app/templates/source_form.html @@ -0,0 +1,21 @@ + + + + + Add Harvest Source + + +

Add Harvest Source

+
+ {{ form.hidden_tag() }} + {{ form.organization_id.label }} {{ form.organization_id() }}

+ {{ form.name.label }} {{ form.name() }}

+ {{ form.emails.label }} {{ form.emails() }}

+ {{ form.url.label }} {{ form.url() }}

+ {{ form.frequency.label }} {{ form.frequency() }}

+ {{ form.schema_type.label }} {{ form.schema_type() }}

+ {{ form.source_type.label }} {{ form.source_type() }}

+ {{ form.submit() }} +
+ + diff --git a/docker-compose.yml b/docker-compose.yml index ba57f9c2..41ef3766 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,9 +59,11 @@ services: environment: DATABASE_URI: ${DATABASE_URI} FLASK_APP: run.py + FLASK_ENV: development ports: - "8080:8080" - command: flask run --host=0.0.0.0 --port=8080 + command: flask run --host=0.0.0.0 --port=8080 --reload + volumes: postgres_data: \ No newline at end of file diff --git a/migrations/versions/701baacbc2f2_base_models.py b/migrations/versions/19ffcfff6080_.py similarity index 78% rename from migrations/versions/701baacbc2f2_base_models.py rename to migrations/versions/19ffcfff6080_.py index 13c34ab3..8ed01328 100644 --- a/migrations/versions/701baacbc2f2_base_models.py +++ b/migrations/versions/19ffcfff6080_.py @@ -1,16 +1,16 @@ -"""base models +"""empty message -Revision ID: 701baacbc2f2 +Revision ID: 19ffcfff6080 Revises: -Create Date: 2024-03-19 21:36:25.741447 +Create Date: 2024-03-25 18:22:17.830851 """ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql + # revision identifiers, used by Alembic. -revision = '701baacbc2f2' +revision = '19ffcfff6080' down_revision = None branch_labels = None depends_on = None @@ -21,33 +21,28 @@ def upgrade(): op.create_table('organization', sa.Column('name', sa.String(), nullable=False), sa.Column('logo', sa.String(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), - nullable=False), + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), sa.PrimaryKeyConstraint('id') ) with op.batch_alter_table('organization', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_organization_name'), - ['name'], unique=False) + batch_op.create_index(batch_op.f('ix_organization_name'), ['name'], unique=False) op.create_table('harvest_source', sa.Column('name', sa.String(), nullable=False), - sa.Column('notification_emails', postgresql.ARRAY(sa.String()), - nullable=True), + sa.Column('notification_emails', sa.String(), nullable=True), sa.Column('organization_id', sa.UUID(), nullable=False), sa.Column('frequency', sa.String(), nullable=False), sa.Column('url', sa.String(), nullable=False), sa.Column('schema_type', sa.String(), nullable=False), sa.Column('source_type', sa.String(), nullable=False), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), - nullable=False), + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), sa.ForeignKeyConstraint(['organization_id'], ['organization.id'], ), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('url') ) op.create_table('harvest_job', sa.Column('harvest_source_id', sa.UUID(), nullable=False), - sa.Column('status', sa.Enum('new', 'in_progress', 'complete', - name='job_status'), nullable=False), + sa.Column('status', sa.Enum('new', 'in_progress', 'complete', name='job_status'), nullable=False), sa.Column('date_created', sa.DateTime(), nullable=True), sa.Column('date_finished', sa.DateTime(), nullable=True), sa.Column('records_added', sa.Integer(), nullable=True), @@ -55,33 +50,27 @@ def upgrade(): sa.Column('records_deleted', sa.Integer(), nullable=True), sa.Column('records_errored', sa.Integer(), nullable=True), sa.Column('records_ignored', sa.Integer(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), - nullable=False), + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), sa.ForeignKeyConstraint(['harvest_source_id'], ['harvest_source.id'], ), sa.PrimaryKeyConstraint('id') ) with op.batch_alter_table('harvest_job', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_harvest_job_date_created'), - ['date_created'], unique=False) - batch_op.create_index(batch_op.f('ix_harvest_job_status'), - ['status'], unique=False) + batch_op.create_index(batch_op.f('ix_harvest_job_date_created'), ['date_created'], unique=False) + batch_op.create_index(batch_op.f('ix_harvest_job_status'), ['status'], unique=False) op.create_table('harvest_error', sa.Column('harvest_job_id', sa.UUID(), nullable=False), sa.Column('harvest_record_id', sa.String(), nullable=True), sa.Column('date_created', sa.DateTime(), nullable=True), sa.Column('type', sa.String(), nullable=True), - sa.Column('severity', sa.Enum('CRITICAL', 'ERROR', 'WARN', - name='error_serverity'), nullable=False), + sa.Column('severity', sa.Enum('CRITICAL', 'ERROR', 'WARN', name='error_serverity'), nullable=False), sa.Column('message', sa.String(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), - nullable=False), + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), sa.ForeignKeyConstraint(['harvest_job_id'], ['harvest_job.id'], ), sa.PrimaryKeyConstraint('id') ) with op.batch_alter_table('harvest_error', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_harvest_error_severity'), - ['severity'], unique=False) + batch_op.create_index(batch_op.f('ix_harvest_error_severity'), ['severity'], unique=False) op.create_table('harvest_record', sa.Column('job_id', sa.UUID(), nullable=False), @@ -89,16 +78,13 @@ def upgrade(): sa.Column('ckan_id', sa.String(), nullable=False), sa.Column('type', sa.String(), nullable=False), sa.Column('source_metadata', sa.String(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), - nullable=False), + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), sa.ForeignKeyConstraint(['job_id'], ['harvest_job.id'], ), - sa.PrimaryKeyConstraint('id') + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('job_id', 'identifier', name='uix_job_id_identifier') ) with op.batch_alter_table('harvest_record', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_harvest_record_ckan_id'), - ['ckan_id'], unique=False) - batch_op.create_index('ix_job_id_identifier', - ['job_id', 'identifier'], unique=False) + batch_op.create_index(batch_op.f('ix_harvest_record_ckan_id'), ['ckan_id'], unique=False) # ### end Alembic commands ### @@ -106,7 +92,6 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table('harvest_record', schema=None) as batch_op: - batch_op.drop_index('ix_job_id_identifier') batch_op.drop_index(batch_op.f('ix_harvest_record_ckan_id')) op.drop_table('harvest_record') diff --git a/requirements.txt b/requirements.txt index ebc69b25..88433016 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ flask==3.0.2 psycopg2-binary==2.9.9 pytest==7.4.4 python-dotenv==1.0.1 +flask-wtf==1.2.1 \ No newline at end of file diff --git a/tests/database/test_db.py b/tests/database/test_db.py index a150bc4f..6e25b416 100644 --- a/tests/database/test_db.py +++ b/tests/database/test_db.py @@ -10,11 +10,9 @@ @pytest.fixture(scope='session') def db_session(): - DATABASE_SERVER = os.getenv("DATABASE_SERVER") DATABASE_URI = os.getenv("DATABASE_URI") TEST_SCHEMA = "test_schema" - modified_uri = DATABASE_URI.replace('@' + DATABASE_SERVER, '@localhost') - engine = create_engine(modified_uri) + engine = create_engine(DATABASE_URI) with engine.connect() as connection: connection.execute(text(f"CREATE SCHEMA IF NOT EXISTS {TEST_SCHEMA};")) From ab834ed60bc869fa995bf6e8a805c9e5023b3d75 Mon Sep 17 00:00:00 2001 From: Jin-Sun-tts Date: Tue, 9 Apr 2024 12:06:29 -0400 Subject: [PATCH 2/3] merge from main --- .env | 1 + .github/workflows/commit.yml | 13 +- .github/workflows/deploy.yml | 4 +- .github/workflows/load_test.yml | 4 +- .github/workflows/publish.yml | 2 +- README.md | 6 +- app/__init__.py | 27 +- app/forms.py | 23 +- app/interface.py | 143 +++- app/models.py | 52 +- app/readme.txt | 32 +- app/routes.py | 47 +- app/templates/harvest_source.html | 88 ++- app/templates/index.html | 135 ++-- app/templates/org_form.html | 28 +- app/templates/source_form.html | 54 +- docker-compose.yml | 2 +- docs/diagrams/mermaid/dest/etl_pipeline-1.svg | 2 +- docs/diagrams/mermaid/src/etl_pipeline.md | 81 ++- harvester/exceptions.py | 4 +- harvester/utils.py | 36 + .../{19ffcfff6080_.py => 112aacfec4f3_.py} | 65 +- poetry.lock | 650 ++++++++++++++++-- pyproject.toml | 4 +- requirements.txt | 3 +- tests/conftest.py | 17 +- tests/database/data.py | 34 - tests/database/test_db.py | 177 ++--- tests/integration/cf/test_cf_tasks_int.py | 28 + tests/unit/cf/test_cf_tasks.py | 47 ++ .../unit/exception/test_exception_handling.py | 5 +- 31 files changed, 1374 insertions(+), 440 deletions(-) rename migrations/versions/{19ffcfff6080_.py => 112aacfec4f3_.py} (70%) delete mode 100644 tests/database/data.py create mode 100644 tests/integration/cf/test_cf_tasks_int.py create mode 100644 tests/unit/cf/test_cf_tasks.py diff --git a/.env b/.env index 028dbe68..52b224bb 100644 --- a/.env +++ b/.env @@ -15,3 +15,4 @@ DATABASE_USER=myuser DATABASE_PASSWORD=mypassword DATABASE_URI=postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_SERVER}:${DATABASE_PORT}/${DATABASE_NAME} +CF_API_URL=https://api.fr.cloud.gov \ No newline at end of file diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 39909dec..723fab66 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -11,22 +11,25 @@ jobs: runs-on: ubuntu-latest name: Python Lint steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: chartboost/ruff-action@v1 test: + environment: development env: CKAN_API_TOKEN_DEV: ${{secrets.CKAN_API_TOKEN_DEV}} + CF_SERVICE_USER: ${{secrets.CF_SERVICE_USER}} + CF_SERVICE_AUTH: ${{secrets.CF_SERVICE_AUTH}} runs-on: ubuntu-latest name: Pytests steps: - name: Check out the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: Set up Python ${{ env.PY_VERSION }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.PY_VERSION }} @@ -41,7 +44,7 @@ jobs: poetry install - name: Setup services - run: docker-compose up -d + run: docker compose up -d - name: Run Pytest run: set -o pipefail; poetry run pytest --junitxml=pytest.xml --cov=harvester ./tests/unit | tee pytest-coverage.txt @@ -61,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Poetry uses: abatilo/actions-poetry@v2 with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3d1378e6..74b493b3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,7 +21,7 @@ runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Poetry uses: abatilo/actions-poetry@v2 with: @@ -79,7 +79,7 @@ runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Poetry uses: abatilo/actions-poetry@v2 with: diff --git a/.github/workflows/load_test.yml b/.github/workflows/load_test.yml index bae7cc74..e8880cc7 100644 --- a/.github/workflows/load_test.yml +++ b/.github/workflows/load_test.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Display Python version diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 276d0b3d..9a436276 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ jobs: url: https://pypi.org/project/datagov-harvesting-logic/ steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Update setup.py if manual release if: github.event_name == 'workflow_dispatch' run: | diff --git a/README.md b/README.md index ffd6400d..5fd87e4a 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,9 @@ If you followed the instructions for `CKAN load testing` and `Harvester testing` 3. when there are database DDL changes, use following steps to generate migration scripts and update database: ```bash - docker-compose db up - docker-compose run app flask db migrate -m "migration description" - docker-compose run app flask db upgrade + docker compose db up + docker compose run app flask db migrate -m "migration description" + docker compose run app flask db upgrade ``` ### Deployment to cloud.gov diff --git a/app/__init__.py b/app/__init__.py index d7884387..9dcc4682 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,22 +3,31 @@ from flask_migrate import Migrate import os from dotenv import load_dotenv +from flask_bootstrap import Bootstrap load_dotenv() DATABASE_URI = os.getenv('DATABASE_URI') -def create_app(): +def create_app(testing=False): app = Flask(__name__) - app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URI - SECRET_KEY = os.urandom(16) - app.config['SECRET_KEY'] = SECRET_KEY + + if testing: + app.config['TESTING'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + else: + app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URI") + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config['SECRET_KEY'] = os.urandom(16) + Bootstrap(app) + db.init_app(app) - - # Initialize Flask-Migrate - Migrate(app, db) - from .routes import register_routes - register_routes(app) + if not testing: + Migrate(app, db) + + from .routes import register_routes + register_routes(app) return app \ No newline at end of file diff --git a/app/forms.py b/app/forms.py index d444a328..dcb6fc1f 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,5 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import StringField, SubmitField, SelectField +from wtforms import StringField, SubmitField, SelectField, TextAreaField from wtforms.validators import DataRequired, URL, ValidationError import re @@ -10,14 +10,23 @@ def validate_email_list(form, field): raise ValidationError("Invalid email address: {}".format(email)) class HarvestSourceForm(FlaskForm): - # organization_id = StringField('organization_id', validators=[DataRequired()]) - organization_id = SelectField('Organization', choices=[], validators=[DataRequired()]) + organization_id = SelectField('Organization', + choices=[], validators=[DataRequired()]) name = StringField('Name', validators=[DataRequired()]) url = StringField('URL', validators=[DataRequired(), URL()]) - emails = StringField('Notification_emails', validators=[DataRequired(), validate_email_list]) - frequency = SelectField('Frequency', choices=['daily', 'monthly', 'yearly'], validators=[DataRequired()]) - schema_type = StringField('Schema Type', validators=[DataRequired()]) - source_type = StringField('Source Type', validators=[DataRequired()]) + emails = TextAreaField('Notification_emails', + validators=[DataRequired(), validate_email_list]) + frequency = SelectField('Frequency', + choices=['Manual', 'Daily', 'Weekly', 'Biweekly','Monthly'], + validators=[DataRequired()]) + user_requested_frequency = StringField('User_requested_frequency', + validators=[DataRequired()]) + schema_type = SelectField('Schema Type', + choices=['strict', 'other'], + validators=[DataRequired()]) + source_type = SelectField('Source Type', + choices=['Datajson', 'WAF'], + validators=[DataRequired()]) submit = SubmitField('Submit') class OrganizationForm(FlaskForm): diff --git a/app/interface.py b/app/interface.py index a1b95d35..99560b2a 100644 --- a/app/interface.py +++ b/app/interface.py @@ -1,13 +1,22 @@ from sqlalchemy import create_engine, inspect from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy.exc import NoResultFound, IntegrityError -from app.models import Organization, HarvestSource, HarvestJob, HarvestError +from sqlalchemy.exc import NoResultFound +from app.models import ( + Organization, HarvestSource, + HarvestJob, HarvestError, HarvestRecord +) from . import DATABASE_URI class HarvesterDBInterface: def __init__(self, session=None): if session is None: - engine = create_engine(DATABASE_URI) + engine = create_engine( + DATABASE_URI, + pool_size=10, + max_overflow=20, + pool_timeout=60, + pool_recycle=1800 + ) session_factory = sessionmaker(bind=engine, autocommit=False, autoflush=False) @@ -34,23 +43,28 @@ def add_organization(self, org_data): def get_all_organizations(self): orgs = self.db.query(Organization).all() - orgs_data = [ - HarvesterDBInterface._to_dict(org) for org in orgs] - return orgs_data + if orgs is None: + return None + else: + orgs_data = [ + HarvesterDBInterface._to_dict(org) for org in orgs] + return orgs_data def get_organization(self, org_id): result = self.db.query(Organization).filter_by(id=org_id).first() + if result is None: + return None return HarvesterDBInterface._to_dict(result) def update_organization(self, org_id, updates): try: - org = self.db.query(Organization).get(org_id) + org = self.db.get(Organization, org_id) for key, value in updates.items(): if hasattr(org, key): setattr(org, key, value) else: - print(f"Warning: Trying to update non-existing field '{key}' in organization") + print(f"Warning: non-existing field '{key}' in organization") self.db.commit() return self._to_dict(org) @@ -60,7 +74,7 @@ def update_organization(self, org_id, updates): return None def delete_organization(self, org_id): - org = self.db.query(Organization).get(org_id) + org = self.db.get(Organization, org_id) if org is None: return "Organization not found" self.db.delete(org) @@ -81,30 +95,38 @@ def add_harvest_source(self, source_data): def get_all_harvest_sources(self): harvest_sources = self.db.query(HarvestSource).all() - harvest_sources_data = [ - HarvesterDBInterface._to_dict(source) for source in harvest_sources] - return harvest_sources_data + if harvest_sources is None: + return None + else: + harvest_sources_data = [ + HarvesterDBInterface._to_dict(source) for source in harvest_sources] + return harvest_sources_data def get_harvest_source(self, source_id): result = self.db.query(HarvestSource).filter_by(id=source_id).first() + if result is None: + return None return HarvesterDBInterface._to_dict(result) def get_harvest_source_by_org(self, org_id): harvest_source = self.db.query( HarvestSource).filter_by(organization_id=org_id).all() - harvest_source_data = [ - HarvesterDBInterface._to_dict(src) for src in harvest_source] - return harvest_source_data + if harvest_source is None: + return None + else: + harvest_source_data = [ + HarvesterDBInterface._to_dict(src) for src in harvest_source] + return harvest_source_data def update_harvest_source(self, source_id, updates): try: - source = self.db.query(HarvestSource).get(source_id) + source = self.db.get(HarvestSource, source_id) for key, value in updates.items(): if hasattr(source, key): setattr(source, key, value) else: - print(f"Warning: Trying to update non-existing field '{key}' in HarvestSource") + print(f"Warning: non-existing field '{key}' in HarvestSource") self.db.commit() return self._to_dict(source) @@ -114,7 +136,7 @@ def update_harvest_source(self, source_id, updates): return None def delete_harvest_source(self, source_id): - source = self.db.query(HarvestSource).get(source_id) + source = self.db.get(HarvestSource, source_id) if source is None: return "Harvest source not found" self.db.delete(source) @@ -133,6 +155,7 @@ def add_harvest_job(self, job_data): self.db.rollback() return None + # for test, will remove later def get_all_harvest_jobs(self): harvest_jobs = self.db.query(HarvestJob).all() harvest_jobs_data = [ @@ -141,24 +164,29 @@ def get_all_harvest_jobs(self): def get_harvest_job(self, job_id): result = self.db.query(HarvestJob).filter_by(id=job_id).first() + if result is None: + return None return HarvesterDBInterface._to_dict(result) def get_harvest_job_by_source(self, source_id): harvest_job = self.db.query( HarvestJob).filter_by(harvest_source_id=source_id).all() - harvest_job_data = [ - HarvesterDBInterface._to_dict(job) for job in harvest_job] - return harvest_job_data + if harvest_job is None: + return None + else: + harvest_job_data = [ + HarvesterDBInterface._to_dict(job) for job in harvest_job] + return harvest_job_data def update_harvest_job(self, job_id, updates): try: - job = self.db.query(HarvestJob).get(job_id) + job = self.db.get(HarvestJob, job_id) for key, value in updates.items(): if hasattr(job, key): setattr(job, key, value) else: - print(f"Warning: Trying to update non-existing field '{key}' in HavestJob") + print(f"Warning: non-existing field '{key}' in HavestJob") self.db.commit() return self._to_dict(job) @@ -168,7 +196,7 @@ def update_harvest_job(self, job_id, updates): return None def delete_harvest_job(self, job_id): - job = self.db.query(HarvestJob).get(job_id) + job = self.db.get(HarvestJob, job_id) if job is None: return "Harvest job not found" self.db.delete(job) @@ -194,18 +222,73 @@ def get_all_harvest_errors(self): HarvesterDBInterface._to_dict(err) for err in harvest_errors] return harvest_errors_data + def get_harvest_error(self, error_id): + result = self.db.query(HarvestError).filter_by(id=error_id).first() + if result is None: + return None + return HarvesterDBInterface._to_dict(result) + def get_harvest_error_by_job(self, job_id): harvest_errors = self.db.query(HarvestError).filter_by(harvest_job_id=job_id) - harvest_errors_data = [ - HarvesterDBInterface._to_dict(err) for err in harvest_errors] - return harvest_errors_data + if harvest_errors is None: + return None + else: + harvest_errors_data = [ + HarvesterDBInterface._to_dict(err) for err in harvest_errors] + return harvest_errors_data - def get_harvest_error(self, error_id): - result = self.db.query(HarvestError).filter_by(id=error_id).first() + + def add_harvest_record(self, record_data): + try: + new_record = HarvestRecord(**record_data) + self.db.add(new_record) + self.db.commit() + self.db.refresh(new_record) + return new_record + except Exception as e: + print("Error:", e) + self.db.rollback() + return None + + # for test, will remove later + def get_all_harvest_records(self): + harvest_records = self.db.query(HarvestRecord).all() + harvest_records_data = [ + HarvesterDBInterface._to_dict(err) for err in harvest_records] + return harvest_records_data + + def get_harvest_record(self, record_id): + result = self.db.query(HarvestRecord).filter_by(id=record_id).first() + if result is None: + return None return HarvesterDBInterface._to_dict(result) + + def get_harvest_record_by_job(self, job_id): + harvest_records = ( + self.db.query(HarvestRecord) + .filter_by(harvest_job_id=job_id) + ) + if harvest_records is None: + return None + else: + harvest_records_data = [ + HarvesterDBInterface._to_dict(rcd) for rcd in harvest_records] + return harvest_records_data + + def get_harvest_record_by_source(self, source_id): + harvest_records = ( + self.db.query(HarvestRecord) + .filter_by(harvest_source_id=source_id) + ) + if harvest_records is None: + return None + else: + harvest_records_data = [ + HarvesterDBInterface._to_dict(rcd) for rcd in harvest_records] + return harvest_records_data def close(self): if hasattr(self.db, "remove"): self.db.remove() elif hasattr(self.db, "close"): - self.db.close() \ No newline at end of file + self.db.close() diff --git a/app/models.py b/app/models.py index 46672a09..6c6e53ce 100644 --- a/app/models.py +++ b/app/models.py @@ -1,15 +1,12 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy.dialects.postgresql import UUID, ARRAY -from sqlalchemy.sql import text -from sqlalchemy import Enum -from sqlalchemy.schema import UniqueConstraint +from sqlalchemy import Enum, func +import uuid db = SQLAlchemy() class Base(db.Model): __abstract__ = True # Indicates that this class should not be created as a table - id = db.Column(UUID(as_uuid=True), primary_key=True, - server_default=text("gen_random_uuid()")) + id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4())) class Organization(Base): __tablename__ = "organization" @@ -25,13 +22,15 @@ class HarvestSource(Base): name = db.Column(db.String, nullable=False) notification_emails = db.Column(db.String) - organization_id = db.Column(UUID(as_uuid=True), + organization_id = db.Column(db.String(36), db.ForeignKey("organization.id"), nullable=False) frequency = db.Column(db.String, nullable=False) + user_requested_frequency = db.Column(db.String) url = db.Column(db.String, nullable=False, unique=True) schema_type = db.Column(db.String, nullable=False) source_type = db.Column(db.String, nullable=False) + status = db.Column(db.String) jobs = db.relationship("HarvestJob", backref="source", cascade="all, delete-orphan", lazy=True) @@ -39,13 +38,13 @@ class HarvestSource(Base): class HarvestJob(Base): __tablename__ = "harvest_job" - harvest_source_id = db.Column(UUID(as_uuid=True), + harvest_source_id = db.Column(db.String(36), db.ForeignKey("harvest_source.id"), nullable=False) status = db.Column(Enum("new", "in_progress", "complete", name="job_status"), nullable=False, index=True) - date_created = db.Column(db.DateTime, index=True) + date_created = db.Column(db.DateTime, index=True, default=func.now()) date_finished = db.Column(db.DateTime) records_added = db.Column(db.Integer) records_updated = db.Column(db.Integer) @@ -59,31 +58,32 @@ class HarvestJob(Base): class HarvestError(Base): __tablename__ = "harvest_error" - harvest_job_id = db.Column(UUID(as_uuid=True), + harvest_job_id = db.Column(db.String(36), db.ForeignKey("harvest_job.id"), nullable=False) - harvest_record_id = db.Column(db.String) - # to-do - # harvest_record_id = db.Column(UUID(as_uuid=True), - # db.ForeignKey("harvest_record.id"), - # nullable=True) - date_created = db.Column(db.DateTime) + harvest_record_id = db.Column(db.String, + db.ForeignKey("harvest_record.id"), + nullable=True) + date_created = db.Column(db.DateTime, default=func.now()) type = db.Column(db.String) severity = db.Column(Enum("CRITICAL", "ERROR", "WARN", name="error_serverity"), nullable=False, index=True) message = db.Column(db.String) + reference = db.Column(db.String) -class HarvestRecord(Base): +class HarvestRecord(db.Model): __tablename__ = "harvest_record" - job_id = db.Column(UUID(as_uuid=True), + id = db.Column(db.String, primary_key=True) + harvest_job_id = db.Column(db.String(36), db.ForeignKey("harvest_job.id"), - nullable=False) - identifier = db.Column(db.String(), nullable=False) - ckan_id = db.Column(db.String(), nullable=False, index=True) - type = db.Column(db.String(), nullable=False) - source_metadata = db.Column(db.String(), nullable=True) - __table_args__ = ( - UniqueConstraint("job_id", "identifier", name="uix_job_id_identifier"), - ) \ No newline at end of file + nullable=True) + harvest_source_id = db.Column(db.String(36), + db.ForeignKey("harvest_source.id"), + nullable=True) + source_hash = db.Column(db.String) + date_created = db.Column(db.DateTime, index=True, default=func.now()) + ckan_id = db.Column(db.String, index=True) + type = db.Column(db.String) + status = db.Column(db.String) \ No newline at end of file diff --git a/app/readme.txt b/app/readme.txt index 8ec993b7..283eb9a6 100644 --- a/app/readme.txt +++ b/app/readme.txt @@ -37,7 +37,7 @@ curl -X POST http://localhost:8080/organization/add -H "Content-Type: applicatio curl -X POST http://localhost:8080/harvest_source/add -H "Content-Type: application/json" -d ' { - "organization_id": "c32e18ee-a854-47be-a05f-42516498a44d", + "organization_id": "4ed9d20a-7de8-4c2d-884f-86b50ec8065d", "name": "Example Harvest Source", "notification_emails": "admin@example.com", "frequency": "daily", @@ -49,23 +49,31 @@ curl -X POST http://localhost:8080/harvest_source/add -H "Content-Type: applicat curl -X POST http://localhost:8080/harvest_job/add -H "Content-Type: application/json" -d ' { - "harvest_source_id": "760129d6-2bf0-4c94-94b9-09622a8a0b23", - "status": "in_progress", - "date_created": "Wed, 27 Mar 2024 20:37:52 GMT" + "harvest_source_id": "59e93b86-83f1-4b70-afa7-c7ca027aeacb", + "status": "in_progress" }' +curl -X POST http://localhost:8080/harvest_record/add -H "Content-Type: application/json" -d ' +{ + "id": "identifier-1", + "harvest_job_id": "a8c03b83-907c-41c9-95aa-d71c3be626b1", + "harvest_source_id": "59e93b86-83f1-4b70-afa7-c7ca027aeacb" +}' curl -X POST http://localhost:8080/harvest_error/add -H "Content-Type: application/json" -d ' { - "harvest_job_id": "aac30640-bd76-46c2-8a64-cf8ee389b190", - "harvest_record_id": "record123", - "date_created": "Wed, 27 Mar 2024 20:37:52 GMT", + "harvest_job_id": "a8c03b83-907c-41c9-95aa-d71c3be626b1", + "harvest_record_id": "identifier-1", "type": "Validation Error", "severity": "ERROR", "message": "Invalid data format." } ' +curl -X GET http://localhost:8080/harvest_job/a8c03b83-907c-41c9-95aa-d71c3be626b1 + + + curl -X DELETE http://localhost:8080/organization/da183992-e598-467a-b245-a3fe8ee2fb91 @@ -99,7 +107,7 @@ curl -X POST https://lharvester-dev-datagov.app.cloud.gov/harvest_job/add -H "Co "date_created": "Wed, 27 Mar 2024 20:37:52 GMT" }' -curl -X PUT https://harvester-dev-datagov.app.cloud.gov/harvest_job/add -H "Content-Type: application/json" -d ' +curl -X PUT https://harvester-dev-datagov.app.cloud.gov/harvest_job/ -H "Content-Type: application/json" -d ' { "status": "complete", "date_finished": "Wed, 27 Mar 2024 22:37:52 GMT", @@ -114,4 +122,10 @@ curl -X PUT https://harvester-dev-datagov.app.cloud.gov/harvest_job/add -H "Cont curl -X PUT https://harvester-dev-datagov.app.cloud.gov/organization/4c456ed3-4717-4933-82c9-d87464063f19 -H "Content-Type: application/json" -d ' { "logo": "url for test 1" -}' \ No newline at end of file +}' + + +curl -X DELETE https://harvester-dev-datagov.app.cloud.gov/organization/e1301d69-d747-4040-9e31-bba7c9508fb9 + + +curl -X DELETE https://harvester-dev-datagov.app.cloud.gov/organization/4c456ed3-4717-4933-82c9-d87464063f19 \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 8832d06b..78ee3801 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,5 @@ -from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify +from flask import Blueprint, request, render_template, jsonify from .interface import HarvesterDBInterface -from tests.database.data import new_org, new_source, new_job, new_error from .forms import HarvestSourceForm, OrganizationForm mod = Blueprint("harvest", __name__) @@ -71,8 +70,9 @@ def add_harvest_source(): if form.validate_on_submit(): new_source = { "name": form.name.data, - "notification_emails": form.emails.data, + "notification_emails": form.emails.data.replace('\r\n', ', '), "frequency": form.frequency.data, + "user_requested_frequency": form.frequency.data, "url": form.url.data, "schema_type": form.schema_type.data, "source_type": form.source_type.data, @@ -191,5 +191,44 @@ def get_harvest_error(error_id=None): except Exception: return "Please provide correct error_id or harvest_job_id" +@mod.route("/harvest_record/add", methods=["POST", "GET"]) +def add_harvest_record(): + if request.is_json: + record = db.add_harvest_record(request.json) + if record: + return jsonify({"message": f"Added new record with ID: {record.id}"}) + else: + return jsonify({"error": "Failed to add harvest record."}), 400 + else: + return jsonify({"Please provide harvest record with json format."}) + +@mod.route("/harvest_record/", methods=["GET"]) +@mod.route("/harvest_record/", methods=["GET"]) +def get_harvest_record(record_id=None): + try: + if record_id: + record = db.get_harvest_record(record_id) + return jsonify(record) if record else ("Not Found", 404) + + job_id = request.args.get("harvest_job_id") + source_id = request.args.get("harvest_source_id") + if job_id: + record = db.get_harvest_record_by_job(job_id) + if not record: + return "No harvest records found for this harvest job", 404 + elif source_id: + record = db.get_harvest_record_by_source(source_id) + if not record: + return "No harvest records found for this harvest source", 404 + else: + # for test, will remove later + record = db.get_all_harvest_records() + + return jsonify(record) + except Exception: + return "Please provide correct record_id or harvest_job_id" + + + def register_routes(app): - app.register_blueprint(mod) \ No newline at end of file + app.register_blueprint(mod) diff --git a/app/templates/harvest_source.html b/app/templates/harvest_source.html index 43eced17..56d821a9 100644 --- a/app/templates/harvest_source.html +++ b/app/templates/harvest_source.html @@ -3,43 +3,69 @@ Harvest Sources + + + -

Harvest Sources

-

+
+

Harvest Sources

+ +
+ + +
+ Get Source + /harvest_source/ +
+ +
+ + +
+ Get Organization Sources + /harvest_source/?organization_id= +
-
- - - - - Get Source + - -
-

- -
- - - -
+ function updateOrgLink() { + var selectedId = document.getElementById('organization_id').value; + var link = document.getElementById('org_link'); + var exampleLink = document.getElementById('example_org_link'); + link.href = selectedId ? `/harvest_source?organization_id=${selectedId}` : "#"; + exampleLink.textContent = + selectedId ? `/harvest_source?organization_id + =${selectedId}` : '/harvest_source/?organization_id='; + } + diff --git a/app/templates/index.html b/app/templates/index.html index 4e70201e..3542181f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,71 +1,104 @@ +# flake8: noqa - Harvest Actions - + Flask routes + -

Harvest Actions

-
    -
  • Add Organization
  • -
  • Add Harvest Source
  • -
    -
  • Add Harvest Job :
  • -

    example:
    - -

    -
    -
  • Add Harvest Error :
  • -

    example:
    - -

    -
    -
  • Get All Organizations
  • -
    -
  • Get All Harvest Sources
  • -
  • Get Harvest Source
  • -
    -
  • Get All Harvest Jobs
  • -
  • Get Harvest Job by id
  • -
  • Get Harvest Job by harvest_source_id
  • -
    -
  • Get All Harvest Errors (for testing)
  • -
  • Get Harvest Error by id
  • -
  • Get All Harvest Errors By Job
  • -
+
  • GET:
  • + + +
  • UPDATE:
  • + + +
  • DELETE:
  • + +
  • Reference for testing:
  • +
  • + Get All Organizations
  • +
  • + Get All Harvest Sources
  • +
  • + Get All Harvest Jobs
  • +
  • + Get All Harvest Records (for testing)
  • +
  • + Get All Harvest Errors (for testing)
  • + + + diff --git a/app/templates/org_form.html b/app/templates/org_form.html index f2b194ca..00241656 100644 --- a/app/templates/org_form.html +++ b/app/templates/org_form.html @@ -3,14 +3,28 @@ Add Organization + -

    Add Organization

    -
    - {{ form.hidden_tag() }} - {{ form.name.label }} {{ form.name() }}

    - {{ form.logo.label }} {{ form.logo() }}

    - {{ form.submit() }} -
    +
    +

    Add Organization

    +
    + {{ form.hidden_tag() }} +
    + {{ form.name.label(class_='form-control-label') }}: + {{ form.name(class_='form-control') }} +
    +
    + {{ form.logo.label(class_='form-control-label') }}: + {{ form.logo(class_='form-control') }} +
    +
    + {{ form.submit(class_='btn btn-primary') }} +
    +
    +
    + + + diff --git a/app/templates/source_form.html b/app/templates/source_form.html index ea2d1204..bd79814f 100644 --- a/app/templates/source_form.html +++ b/app/templates/source_form.html @@ -3,19 +3,49 @@ Add Harvest Source + -

    Add Harvest Source

    -
    - {{ form.hidden_tag() }} - {{ form.organization_id.label }} {{ form.organization_id() }}

    - {{ form.name.label }} {{ form.name() }}

    - {{ form.emails.label }} {{ form.emails() }}

    - {{ form.url.label }} {{ form.url() }}

    - {{ form.frequency.label }} {{ form.frequency() }}

    - {{ form.schema_type.label }} {{ form.schema_type() }}

    - {{ form.source_type.label }} {{ form.source_type() }}

    - {{ form.submit() }} -
    +
    +

    Add Harvest Source

    +
    + {{ form.hidden_tag() }} +
    + {{ form.organization_id.label(class_='form-control-label') }}: + {{ form.organization_id(class_='form-control') }} +
    +
    + {{ form.name.label(class_='form-control-label') }}: + {{ form.name(class_='form-control') }} +
    +
    + {{ form.emails.label(class_='form-control-label') }}: + {{ form.emails(class_='form-control') }} +
    +
    + {{ form.url.label(class_='form-control-label') }}: + {{ form.url(class_='form-control') }} +
    +
    + {{ form.frequency.label(class_='form-control-label') }}: + {{ form.frequency(class_='form-control') }} +
    +
    + {{ form.user_requested_frequency.label(class_='form-control-label') }}: + {{ form.user_requested_frequency(class_='form-control') }} +
    +
    + {{ form.schema_type.label(class_='form-control-label') }}: + {{ form.schema_type(class_='form-control') }} +
    +
    + {{ form.source_type.label(class_='form-control-label') }}: + {{ form.source_type(class_='form-control') }} +
    + +
    +
    + + diff --git a/docker-compose.yml b/docker-compose.yml index 41ef3766..2e7eeffe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,7 +57,7 @@ services: volumes: - .:/app environment: - DATABASE_URI: ${DATABASE_URI} + DATABASE_URI: postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@db:${DATABASE_PORT}/${DATABASE_NAME} FLASK_APP: run.py FLASK_ENV: development ports: diff --git a/docs/diagrams/mermaid/dest/etl_pipeline-1.svg b/docs/diagrams/mermaid/dest/etl_pipeline-1.svg index bd7bd1e7..d9c5882b 100644 --- a/docs/diagrams/mermaid/dest/etl_pipeline-1.svg +++ b/docs/diagrams/mermaid/dest/etl_pipeline-1.svg @@ -1 +1 @@ -AirflowSESMDTranslatorCKANDatagov Harvesting LogicDAGHarvest DBSESMDTranslatorCKANDatagov Harvesting LogicDAGHarvest DBloop[EXTRACT source &COMPARE datasets]loop[DELETE items to delete]*for non-dcat sourcesloop[TRANSFORM items to transform]loop[VALIDATE itemscreate/update]loop[LOAD items to create/update]Harvest triggered/scheduled1Harvest Source Config(HSC)2Trigger Extract(HSC)3Fetch source4Generate lists to Create/Update/Delete5Return metrics on Create/Update/Delete6Trigger Delete(HSC)7CKAN Delete API(Identifier)8Return metrics on Delete operation9Trigger Tranform(HSC)10MDTransform(dataset)11Transformed Item12Return metrics on Transform operation13Trigger Validation(HSC)14Validate against schema15Return validation metrics16Trigger Load(HSC)17CKAN package_create(Item)18Return load metrics19Compile job metrics20POST job metrics to Harvest DB21Email job metrics(jobMetrics, listOfEmails)22 \ No newline at end of file +SESCKANS3MDTranslatorDatagov Harvesting LogicFlask AppHarvest DBAgencyHarvest SourceSESCKANS3MDTranslatorDatagov Harvesting LogicFlask AppHarvest DBAgencyHarvest SourceTRIGGER HARVESTINVOKE HARVEST JOBEXTRACTCOMPAREloop[hash source record andCOMPARE with activerecords' <<source_hash>>]TRANSFORM*for non-dcat sourcesloop[items to transform]PUT TO S3DELETEloop[DELETE items to delete]VALIDATEloop[VALIDATE itemsto create/update]SYNCloop[SYNC items to create/update]COMPLETEPOST-PROCESSINGActorvia GH Action,or manual button in Flask appwith corresponding <<harvest_source_id>>1create harvest_job2invoke harvest.py with corresponding <<source_id>>3returns OK4update job_status: in_progress5Fetch source from <<source_url>>6return source7Fetch records from db8Return active recordswith corresponding <<harvest_source_id>>filtered by most recent TIMESTAMP9Generate lists to Create/Update/Delete10Write records with status: create, update, delete11MDTransform(dataset)12Transformed Item13Log failures as harvest_error with type: transformupdate harvest_record status: error_transform14write source_metadata (plus transform artifact) to S3S3://{BUCKET_PREFIX}/{HARVEST_SOURCE_ID}/{UNIQUE_IDENTIFIER}15CKAN Delete API(Identifier)16Log failures as harvest_error with type: deletionupdate harvest_record status: error_delete17Validate against schema18Log failures as harvest_error with type: validationupdate harvest_record status: error_validation19CKAN package_create or package_update (Identifier)20Log failures as harvest_error with type: syncupdate harvest_record status: error_sync21POST job metrics to harvest_job table (jobId)22Trigger email /api/report (jobId)23Update harvest_job with status: complete24Fetch harvest_source (harvest_source_id)25return <<harvest_source>>26Fetch harvest_job (job_id)27Return <<harvest_job>>28Email job metrics (jobMetrics, notification_emails)29Actor \ No newline at end of file diff --git a/docs/diagrams/mermaid/src/etl_pipeline.md b/docs/diagrams/mermaid/src/etl_pipeline.md index 845fa16c..4451bf93 100644 --- a/docs/diagrams/mermaid/src/etl_pipeline.md +++ b/docs/diagrams/mermaid/src/etl_pipeline.md @@ -1,50 +1,63 @@ ```mermaid sequenceDiagram autonumber + actor A as Actor participant HDB as Harvest DB - box transparent Airflow - participant DAG + participant FA as Flask App participant DHL as Datagov Harvesting Logic - end - participant CKAN participant MD as MDTranslator + participant HS as Agency
    Harvest Source + participant S3 + participant CKAN participant SES - DAG->>HDB: Harvest triggered/
    scheduled - HDB-->>DAG: Harvest Source Config(HSC) - DAG->>DHL: Trigger Extract(HSC) - DHL->>DHL: Fetch source - loop EXTRACT source & COMPARE datasets + note over A: TRIGGER HARVEST + A->>FA: via GH Action,
    or manual button in Flask app
    with corresponding <> + note over FA: INVOKE HARVEST JOB + FA->>HDB: create harvest_job + FA->>+DHL: invoke harvest.py
    with corresponding <> + DHL-->>-FA: returns OK + FA->>HDB: update job_status: in_progress + note over DHL: EXTRACT + DHL->>+HS: Fetch source from <> + HS->>-DHL: return source + DHL->>+HDB: Fetch records from db + HDB-->>-DHL: Return active records
    with corresponding <>
    filtered by most recent TIMESTAMP + note over DHL: COMPARE + loop hash source record and COMPARE with active records' <> DHL->>DHL: Generate lists to Create/Update/Delete end - DHL-->>DAG: Return metrics on Create/Update/Delete - DAG->>DHL: Trigger Delete(HSC) + DHL->>HDB: Write records with status: create, update, delete + note over DHL: TRANSFORM
    *for non-dcat sources + loop items to transform + DHL->>+MD: MDTransform(dataset) + MD-->>-DHL: Transformed Item + end + DHL-->>HDB: Log failures as harvest_error with type: transform
    update harvest_record status: error_transform + note over DHL: PUT TO S3 + DHL->>S3: write source_metadata (plus transform artifact) to S3
    S3://{BUCKET_PREFIX}/{HARVEST_SOURCE_ID}/{UNIQUE_IDENTIFIER} + note over DHL: DELETE loop DELETE items to delete DHL->>CKAN: CKAN Delete API(Identifier) end - DHL-->>DAG: Return metrics on Delete operation - - rect rgba(0, 0, 255, .1) - note right of DAG: *for non-dcat sources - DAG->>DHL: Trigger Tranform(HSC) - loop TRANSFORM items to transform - DHL->>MD: MDTransform(dataset) - MD-->>DHL: Transformed Item - end - DHL-->>DAG: Return metrics on Transform operation - end - DAG->>DHL: Trigger Validation(HSC) - loop VALIDATE items create/update + DHL-->>HDB: Log failures as harvest_error with type: deletion
    update harvest_record status: error_delete + note over DHL: VALIDATE + loop VALIDATE items to create/update DHL->>DHL: Validate against schema end - DHL-->>DAG: Return validation metrics - DAG->>DHL: Trigger Load(HSC) - - loop LOAD items to create/update - DHL->>CKAN: CKAN package_create(Item) + DHL-->>HDB: Log failures as harvest_error with type: validation
    update harvest_record status: error_validation + note over DHL: SYNC + loop SYNC items to create/update + DHL->>CKAN: CKAN package_create or package_update (Identifier) end - DHL-->>DAG: Return load metrics - DAG->>DAG: Compile job metrics - DAG->>HDB: POST job metrics to Harvest DB - DAG->>SES: Email job metrics(jobMetrics, listOfEmails) - + DHL-->>HDB: Log failures as harvest_error with type: sync
    update harvest_record status: error_sync + DHL->>HDB: POST job metrics to harvest_job table (jobId) + DHL-)FA: Trigger email /api/report (jobId) + DHL->>HDB: Update harvest_job with status: complete + note over DHL: COMPLETE + note over FA: POST-PROCESSING + FA->>HDB: Fetch harvest_source (harvest_source_id) + HDB-->>FA: return <> + FA->>HDB: Fetch harvest_job (job_id) + HDB-->>FA: Return <> + FA->>SES: Email job metrics (jobMetrics, notification_emails) ``` diff --git a/harvester/exceptions.py b/harvester/exceptions.py index 2fccd18d..1bf76e24 100644 --- a/harvester/exceptions.py +++ b/harvester/exceptions.py @@ -24,7 +24,7 @@ def __init__(self, msg, harvest_job_id): "date_created": datetime.utcnow(), } - self.db_interface.add_harvest_error(error_data, self.harvest_job_id) + self.db_interface.add_harvest_error(error_data) self.logger.critical(self.msg, exc_info=True) @@ -63,7 +63,7 @@ def __init__(self, msg, harvest_job_id, title): "harvest_record_id": self.title # to-do } - self.db_interface.add_harvest_error(error_data, self.harvest_job_id) + self.db_interface.add_harvest_error(error_data) self.logger.error(self.msg, exc_info=True) diff --git a/harvester/utils.py b/harvester/utils.py index 51614b0e..4f230c1f 100644 --- a/harvester/utils.py +++ b/harvester/utils.py @@ -5,6 +5,9 @@ import boto3 import sansjson +from cloudfoundry_client.client import CloudFoundryClient +from cloudfoundry_client.v3.tasks import TaskManager + # ruff: noqa: F841 @@ -71,3 +74,36 @@ def put_object(self, body: str, key_name: str): "ContentType": "application/json", } ) + + +class CFHandler: + def __init__(self, url: str = None, user: str = None, password: str = None): + self.target_endpoint = url if url is not None else os.getenv("CF_API_URL") + self.client = CloudFoundryClient(self.target_endpoint) + self.client.init_with_user_credentials( + user if user is not None else os.getenv("CF_SERVICE_USER"), + password if password is not None else os.getenv("CF_SERVICE_AUTH"), + ) + + self.task_mgr = TaskManager(self.target_endpoint, self.client) + + def start_task(self, app_guuid, command, task_id): + return self.task_mgr.create(app_guuid, command, task_id) + + def stop_task(self, task_id): + return self.task_mgr.cancel(task_id) + + def get_task(self, task_id): + return self.task_mgr.get(task_id) + + def get_all_app_tasks(self, app_guuid): + return [task for task in self.client.v3.apps[app_guuid].tasks()] + + def get_all_running_tasks(self, tasks): + return sum(1 for _ in filter(lambda task: task["state"] == "RUNNING", tasks)) + + def read_recent_app_logs(self, app_guuid, task_id=None): + + app = self.client.v2.apps[app_guuid] + logs = filter(lambda lg: task_id in lg, [str(log) for log in app.recent_logs()]) + return "\n".join(logs) diff --git a/migrations/versions/19ffcfff6080_.py b/migrations/versions/112aacfec4f3_.py similarity index 70% rename from migrations/versions/19ffcfff6080_.py rename to migrations/versions/112aacfec4f3_.py index 8ed01328..1c58f6a5 100644 --- a/migrations/versions/19ffcfff6080_.py +++ b/migrations/versions/112aacfec4f3_.py @@ -1,8 +1,9 @@ +# flake8: noqa """empty message -Revision ID: 19ffcfff6080 +Revision ID: 112aacfec4f3 Revises: -Create Date: 2024-03-25 18:22:17.830851 +Create Date: 2024-04-08 16:31:41.323203 """ from alembic import op @@ -10,7 +11,7 @@ # revision identifiers, used by Alembic. -revision = '19ffcfff6080' +revision = '112aacfec4f3' down_revision = None branch_labels = None depends_on = None @@ -21,7 +22,7 @@ def upgrade(): op.create_table('organization', sa.Column('name', sa.String(), nullable=False), sa.Column('logo', sa.String(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('id', sa.String(length=36), nullable=False), sa.PrimaryKeyConstraint('id') ) with op.batch_alter_table('organization', schema=None) as batch_op: @@ -30,18 +31,20 @@ def upgrade(): op.create_table('harvest_source', sa.Column('name', sa.String(), nullable=False), sa.Column('notification_emails', sa.String(), nullable=True), - sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.String(length=36), nullable=False), sa.Column('frequency', sa.String(), nullable=False), + sa.Column('user_requested_frequency', sa.String(), nullable=True), sa.Column('url', sa.String(), nullable=False), sa.Column('schema_type', sa.String(), nullable=False), sa.Column('source_type', sa.String(), nullable=False), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('status', sa.String(), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), sa.ForeignKeyConstraint(['organization_id'], ['organization.id'], ), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('url') ) op.create_table('harvest_job', - sa.Column('harvest_source_id', sa.UUID(), nullable=False), + sa.Column('harvest_source_id', sa.String(length=36), nullable=False), sa.Column('status', sa.Enum('new', 'in_progress', 'complete', name='job_status'), nullable=False), sa.Column('date_created', sa.DateTime(), nullable=True), sa.Column('date_finished', sa.DateTime(), nullable=True), @@ -50,7 +53,7 @@ def upgrade(): sa.Column('records_deleted', sa.Integer(), nullable=True), sa.Column('records_errored', sa.Integer(), nullable=True), sa.Column('records_ignored', sa.Integer(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('id', sa.String(length=36), nullable=False), sa.ForeignKeyConstraint(['harvest_source_id'], ['harvest_source.id'], ), sa.PrimaryKeyConstraint('id') ) @@ -58,47 +61,53 @@ def upgrade(): batch_op.create_index(batch_op.f('ix_harvest_job_date_created'), ['date_created'], unique=False) batch_op.create_index(batch_op.f('ix_harvest_job_status'), ['status'], unique=False) + op.create_table('harvest_record', + sa.Column('id', sa.String(), nullable=False), + sa.Column('harvest_job_id', sa.String(length=36), nullable=True), + sa.Column('harvest_source_id', sa.String(length=36), nullable=True), + sa.Column('source_hash', sa.String(), nullable=True), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.Column('ckan_id', sa.String(), nullable=True), + sa.Column('type', sa.String(), nullable=True), + sa.Column('status', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['harvest_job_id'], ['harvest_job.id'], ), + sa.ForeignKeyConstraint(['harvest_source_id'], ['harvest_source.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('harvest_record', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_harvest_record_ckan_id'), ['ckan_id'], unique=False) + batch_op.create_index(batch_op.f('ix_harvest_record_date_created'), ['date_created'], unique=False) + op.create_table('harvest_error', - sa.Column('harvest_job_id', sa.UUID(), nullable=False), + sa.Column('harvest_job_id', sa.String(length=36), nullable=False), sa.Column('harvest_record_id', sa.String(), nullable=True), sa.Column('date_created', sa.DateTime(), nullable=True), sa.Column('type', sa.String(), nullable=True), sa.Column('severity', sa.Enum('CRITICAL', 'ERROR', 'WARN', name='error_serverity'), nullable=False), sa.Column('message', sa.String(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('reference', sa.String(), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), sa.ForeignKeyConstraint(['harvest_job_id'], ['harvest_job.id'], ), + sa.ForeignKeyConstraint(['harvest_record_id'], ['harvest_record.id'], ), sa.PrimaryKeyConstraint('id') ) with op.batch_alter_table('harvest_error', schema=None) as batch_op: batch_op.create_index(batch_op.f('ix_harvest_error_severity'), ['severity'], unique=False) - op.create_table('harvest_record', - sa.Column('job_id', sa.UUID(), nullable=False), - sa.Column('identifier', sa.String(), nullable=False), - sa.Column('ckan_id', sa.String(), nullable=False), - sa.Column('type', sa.String(), nullable=False), - sa.Column('source_metadata', sa.String(), nullable=True), - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.ForeignKeyConstraint(['job_id'], ['harvest_job.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('job_id', 'identifier', name='uix_job_id_identifier') - ) - with op.batch_alter_table('harvest_record', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_harvest_record_ckan_id'), ['ckan_id'], unique=False) - # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('harvest_record', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_harvest_record_ckan_id')) - - op.drop_table('harvest_record') with op.batch_alter_table('harvest_error', schema=None) as batch_op: batch_op.drop_index(batch_op.f('ix_harvest_error_severity')) op.drop_table('harvest_error') + with op.batch_alter_table('harvest_record', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_harvest_record_date_created')) + batch_op.drop_index(batch_op.f('ix_harvest_record_ckan_id')) + + op.drop_table('harvest_record') with op.batch_alter_table('harvest_job', schema=None) as batch_op: batch_op.drop_index(batch_op.f('ix_harvest_job_status')) batch_op.drop_index(batch_op.f('ix_harvest_job_date_created')) diff --git a/poetry.lock b/poetry.lock index a7a7f8c6..21945ba2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,119 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, + {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, + {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, + {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, + {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, + {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, + {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, + {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, + {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, + {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, + {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, + {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" [[package]] name = "alembic" version = "1.13.1" description = "A database migration tool for SQLAlchemy." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -20,11 +129,21 @@ typing-extensions = ">=4" [package.extras] tz = ["backports.zoneinfo"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + [[package]] name = "attrs" version = "23.2.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -44,7 +163,6 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p name = "beautifulsoup4" version = "4.12.3" description = "Screen-scraping library" -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -66,7 +184,6 @@ lxml = ["lxml"] name = "blinker" version = "1.7.0" description = "Fast, simple object-to-object and broadcast signaling" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -78,7 +195,6 @@ files = [ name = "boto3" version = "1.34.54" description = "The AWS SDK for Python" -category = "main" optional = false python-versions = ">= 3.8" files = [ @@ -98,7 +214,6 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] name = "botocore" version = "1.34.54" description = "Low-level, data-driven core of boto 3." -category = "main" optional = false python-versions = ">= 3.8" files = [ @@ -118,7 +233,6 @@ crt = ["awscrt (==0.19.19)"] name = "certifi" version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -130,7 +244,6 @@ files = [ name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -230,7 +343,6 @@ files = [ name = "ckanapi" version = "4.7" description = "A command line interface and Python module for accessing the CKAN Action API" -category = "main" optional = false python-versions = "*" files = [ @@ -249,7 +361,6 @@ six = ">=1.9,<2.0" name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -260,11 +371,30 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "cloudfoundry-client" +version = "1.36.0" +description = "A client library for CloudFoundry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cloudfoundry-client-1.36.0.tar.gz", hash = "sha256:9d087ad114ee68b2153917a03a7a724513f17a3c1299f2957a4ace109b1665bd"}, + {file = "cloudfoundry_client-1.36.0-py3-none-any.whl", hash = "sha256:790396326d6af17728a69495a1921865bf5cdf91bac173ec2157a1a02e774e96"}, +] + +[package.dependencies] +aiohttp = ">=3.8.0" +oauth2-client = "1.4.2" +polling2 = "0.5.0" +protobuf = ">=3.20.0,<5.0.0dev" +PyYAML = ">=6.0" +requests = ">=2.5.0" +websocket-client = ">=1.7.0,<1.8.0" + [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -276,7 +406,6 @@ files = [ name = "coverage" version = "7.4.3" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -344,7 +473,6 @@ toml = ["tomli"] name = "deepdiff" version = "6.7.1" description = "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -363,18 +491,28 @@ optimize = ["orjson"] name = "docopt" version = "0.6.2" description = "Pythonic argument parser, that will make you smile" -category = "main" optional = false python-versions = "*" files = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] +[[package]] +name = "dominate" +version = "2.9.1" +description = "Dominate is a Python library for creating and manipulating HTML documents using an elegant DOM API." +category = "main" +optional = false +python-versions = ">=3.4" +files = [ + {file = "dominate-2.9.1-py2.py3-none-any.whl", hash = "sha256:cb7b6b79d33b15ae0a6e87856b984879927c7c2ebb29522df4c75b28ffd9b989"}, + {file = "dominate-2.9.1.tar.gz", hash = "sha256:558284687d9b8aae1904e3d6051ad132dd4a8c0cf551b37ea4e7e42a31d19dc4"}, +] + [[package]] name = "exceptiongroup" version = "1.2.0" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -389,7 +527,6 @@ test = ["pytest (>=6)"] name = "flask" version = "3.0.2" description = "A simple framework for building complex web applications." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -408,11 +545,26 @@ Werkzeug = ">=3.0.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[package]] +name = "flask-bootstrap" +version = "3.3.7.1" +description = "An extension that includes Bootstrap in your project, without any boilerplate code." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "Flask-Bootstrap-3.3.7.1.tar.gz", hash = "sha256:cb08ed940183f6343a64e465e83b3a3f13c53e1baabb8d72b5da4545ef123ac8"}, +] + +[package.dependencies] +dominate = "*" +Flask = ">=0.8" +visitor = "*" + [[package]] name = "flask-migrate" version = "4.0.7" description = "SQLAlchemy database migrations for Flask applications using Alembic." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -429,7 +581,6 @@ Flask-SQLAlchemy = ">=1.0" name = "flask-sqlalchemy" version = "3.1.1" description = "Add SQLAlchemy support to your Flask application." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -445,7 +596,6 @@ sqlalchemy = ">=2.0.16" name = "flask-wtf" version = "1.2.1" description = "Form rendering, validation, and CSRF protection for Flask with WTForms." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -461,11 +611,96 @@ wtforms = "*" [package.extras] email = ["email-validator"] +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + [[package]] name = "greenlet" version = "3.0.3" description = "Lightweight in-process concurrent programming" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -537,7 +772,6 @@ test = ["objgraph", "psutil"] name = "idna" version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -549,7 +783,6 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -561,7 +794,6 @@ files = [ name = "itsdangerous" version = "2.1.2" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -573,7 +805,6 @@ files = [ name = "jinja2" version = "3.1.3" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -591,7 +822,6 @@ i18n = ["Babel (>=2.7)"] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -603,7 +833,6 @@ files = [ name = "jsonschema" version = "4.21.1" description = "An implementation of JSON Schema validation for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -625,7 +854,6 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "jsonschema-specifications" version = "2023.12.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -640,7 +868,6 @@ referencing = ">=0.31.0" name = "mako" version = "1.3.2" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -660,7 +887,6 @@ testing = ["pytest"] name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -726,11 +952,123 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "oauth2-client" +version = "1.4.2" +description = "A client library for OAuth2" +optional = false +python-versions = "*" +files = [ + {file = "oauth2-client-1.4.2.tar.gz", hash = "sha256:5381900448ff1ae762eb7c65c501002eac46bb5ca2f49477fdfeaf9e9969f284"}, + {file = "oauth2_client-1.4.2-py3-none-any.whl", hash = "sha256:7b938ba8166128a3c4c15ad23ca0c95a2468f8e8b6069d019ebc73360c15c7ca"}, +] + +[package.dependencies] +requests = ">=2.5.0" + [[package]] name = "ordered-set" version = "4.1.0" description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -745,7 +1083,6 @@ dev = ["black", "mypy", "pytest"] name = "packaging" version = "23.2" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -757,7 +1094,6 @@ files = [ name = "pluggy" version = "1.4.0" description = "plugin and hook calling mechanisms for python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -769,11 +1105,41 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "polling2" +version = "0.5.0" +description = "Updated polling utility with many configurable options" +optional = false +python-versions = "*" +files = [ + {file = "polling2-0.5.0-py2.py3-none-any.whl", hash = "sha256:ad86d56fbd7502f0856cac2d0109d595c18fa6c7fb12c88cee5e5d16c17286c1"}, + {file = "polling2-0.5.0.tar.gz", hash = "sha256:90b7da82cf7adbb48029724d3546af93f21ab6e592ec37c8c4619aedd010e342"}, +] + +[[package]] +name = "protobuf" +version = "4.25.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, +] + [[package]] name = "psycopg2-binary" version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -855,7 +1221,6 @@ files = [ name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -878,7 +1243,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -897,7 +1261,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -912,7 +1275,6 @@ six = ">=1.5" name = "python-dotenv" version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -927,7 +1289,6 @@ cli = ["click (>=5.0)"] name = "python-slugify" version = "8.0.4" description = "A Python slugify application that also handles Unicode" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -941,11 +1302,70 @@ text-unidecode = ">=1.3" [package.extras] unidecode = ["Unidecode (>=1.1.1)"] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + [[package]] name = "referencing" version = "0.33.0" description = "JSON Referencing + Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -961,7 +1381,6 @@ rpds-py = ">=0.7.0" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -983,7 +1402,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "rpds-py" version = "0.18.0" description = "Python bindings to Rust's persistent data structures (rpds)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1092,7 +1510,6 @@ files = [ name = "ruff" version = "0.0.261" description = "An extremely fast Python linter, written in Rust." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1119,7 +1536,6 @@ files = [ name = "s3transfer" version = "0.10.0" description = "An Amazon S3 Transfer Manager" -category = "main" optional = false python-versions = ">= 3.8" files = [ @@ -1137,7 +1553,6 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] name = "sansjson" version = "0.3.0" description = "Your friendly neighborhood JSON sorter helper" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1149,7 +1564,6 @@ files = [ name = "setuptools" version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1166,7 +1580,6 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar name = "simplejson" version = "3.19.2" description = "Simple, fast, extensible JSON encoder/decoder for Python" -category = "main" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1274,7 +1687,6 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1286,7 +1698,6 @@ files = [ name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1298,7 +1709,6 @@ files = [ name = "sqlalchemy" version = "2.0.28" description = "Database Abstraction Library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1354,7 +1764,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} typing-extensions = ">=4.6.0" [package.extras] @@ -1386,7 +1796,6 @@ sqlcipher = ["sqlcipher3_binary"] name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" -category = "main" optional = false python-versions = "*" files = [ @@ -1398,7 +1807,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1410,7 +1818,6 @@ files = [ name = "typing-extensions" version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1422,7 +1829,6 @@ files = [ name = "urllib3" version = "2.0.7" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1436,11 +1842,37 @@ secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17. socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "visitor" +version = "0.1.3" +description = "A tiny pythonic visitor implementation." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "visitor-0.1.3.tar.gz", hash = "sha256:2c737903b2b6864ebc6167eef7cf3b997126f1aa94bdf590f90f1436d23e480a"}, +] + +[[package]] +name = "websocket-client" +version = "1.7.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "werkzeug" version = "3.0.1" description = "The comprehensive WSGI web application library." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1458,7 +1890,6 @@ watchdog = ["watchdog (>=2.3)"] name = "wtforms" version = "3.1.2" description = "Form validation and rendering for Python web development." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1472,7 +1903,110 @@ markupsafe = "*" [package.extras] email = ["email-validator"] +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "455443e5c7bb03de7b2d454997bb6c971f6e6f74397226c2808e5a09e521ce89" +content-hash = "caee85df1023e73c48fa835d0f7b3d39fc645d0cb3e1c01b4f717f7d8d4f23f3" diff --git a/pyproject.toml b/pyproject.toml index 213f8a7f..79b69b17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "datagov-harvesting-logic" -version = "0.3.4" +version = "0.3.6" description = "" # authors = [ # {name = "Jin Sun", email = "jin.sun@gsa.gov"}, @@ -33,6 +33,8 @@ psycopg2-binary = "^2.9.9" flask-sqlalchemy = "^3.1.1" flask-wtf = "^1.2.1" flask-migrate = "^4.0.7" +flask-bootstrap = "^3.3.7.1" +cloudfoundry-client = "^1.36.0" [tool.poetry.group.dev.dependencies] pytest = "^7.3.0" diff --git a/requirements.txt b/requirements.txt index 88433016..efdcbcde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ flask==3.0.2 psycopg2-binary==2.9.9 pytest==7.4.4 python-dotenv==1.0.1 -flask-wtf==1.2.1 \ No newline at end of file +flask-wtf==1.2.1 +flask_bootstrap \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 1e0dbf1b..a0a27f2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,10 +6,11 @@ from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker, scoped_session -from app.models import Base +from app.models import Base from app.interface import HarvesterDBInterface from harvester.utils import open_json +from harvester.utils import CFHandler load_dotenv() @@ -108,3 +109,17 @@ def dcatus_compare_config() -> dict: @pytest.fixture def ckan_compare() -> dict: return open_json(HARVEST_SOURCES / "dcatus" / "ckan_datasets_resp.json")["results"] + + +@pytest.fixture +def cf_handler() -> CFHandler: + return CFHandler() + + +@pytest.fixture +def dhl_cf_task_data() -> dict: + return { + "app_guuid": "f4ab7f86-bee0-44fd-8806-1dca7f8e215a", + "task_id": "cf_task_integration", + "command": "/usr/bin/sleep 60", + } diff --git a/tests/database/data.py b/tests/database/data.py deleted file mode 100644 index 7e7d7fe9..00000000 --- a/tests/database/data.py +++ /dev/null @@ -1,34 +0,0 @@ -from datetime import datetime - -new_org = { - 'name': 'GSA', - 'logo' : 'url for the logo' -} - -new_source = { - 'name': 'Example Harvest Source', - 'notification_emails': ['admin@example.com'], - 'frequency': 'daily', - 'url': "http://example.com", - 'schema_type': 'strict', - 'source_type': 'json' -} - -new_job = { - "status": "in_progress", - "date_created": datetime.utcnow(), - "date_finished": datetime.utcnow(), - "records_added": 100, - "records_updated": 20, - "records_deleted": 5, - "records_errored": 3, - "records_ignored": 1 -} - -new_error = { - "harvest_record_id": "record123", - "date_created": datetime.utcnow(), - "type": "Validation Error", - "severity": "ERROR", - "message": "Invalid data format." -} \ No newline at end of file diff --git a/tests/database/test_db.py b/tests/database/test_db.py index 6e25b416..e14020ca 100644 --- a/tests/database/test_db.py +++ b/tests/database/test_db.py @@ -1,89 +1,112 @@ import pytest -from sqlalchemy import create_engine, text -from sqlalchemy.orm import sessionmaker, scoped_session -from app.models import Base +from app import create_app +from app.models import db from app.interface import HarvesterDBInterface -from dotenv import load_dotenv -import os - -load_dotenv() +from sqlalchemy.orm import scoped_session, sessionmaker @pytest.fixture(scope='session') -def db_session(): - DATABASE_URI = os.getenv("DATABASE_URI") - TEST_SCHEMA = "test_schema" - engine = create_engine(DATABASE_URI) +def app(): + _app = create_app(testing=True) + _app.config['TESTING'] = True + _app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' - with engine.connect() as connection: - connection.execute(text(f"CREATE SCHEMA IF NOT EXISTS {TEST_SCHEMA};")) - connection.execute(text(f"SET search_path TO {TEST_SCHEMA};")) + with _app.app_context(): + db.create_all() + yield _app + db.drop_all() - Base.metadata.create_all(engine) - SessionLocal = sessionmaker(bind=engine) +@pytest.fixture(scope='function') +def session(app): + with app.app_context(): + connection = db.engine.connect() + transaction = connection.begin() - session = scoped_session(SessionLocal) - yield session + SessionLocal = sessionmaker(bind=connection, autocommit=False, autoflush=False) + session = scoped_session(SessionLocal) + yield session - session.remove() + session.remove() + transaction.rollback() + connection.close() - with engine.begin() as connection: - connection.execute(text(f"DROP SCHEMA IF EXISTS {TEST_SCHEMA} CASCADE;")) +@pytest.fixture(scope='function') +def interface(session): + return HarvesterDBInterface(session=session) - engine.dispose() -@pytest.fixture(scope='session') -def db_interface(db_session): - return HarvesterDBInterface(db_session) - -def test_add_harvest_source(db_interface): - org_data = db_interface.add_organization({'name': 'GSA', - 'logo': 'url for the logo'}) - source_data = {'name': 'Test Source', - 'frequency': 'daily', - 'url': "http://example-1.com", - 'schema_type': 'strict', - 'source_type': 'json'} - new_source = db_interface.add_harvest_source(source_data, str(org_data.id)) - assert new_source.name == 'Test Source' - -def test_add_and_get_harvest_source(db_interface): - org_data = db_interface.add_organization({'name': 'GSA', - 'logo': 'url for the logo'}) - new_source = db_interface.add_harvest_source({ - 'name': 'Test Source', - 'notification_emails': ['test@example.com'], - 'frequency': 'daily', - 'url': "http://example-2.com", - 'schema_type': 'strict', - 'source_type': 'json' - }, str(org_data.id)) - assert new_source.name == 'Test Source' - - sources = db_interface.get_all_harvest_sources() - assert any(source['name'] == 'Test Source' for source in sources) - - -def test_add_harvest_job(db_interface): - org_data = db_interface.add_organization({'name': 'GSA', - 'logo': 'url for the logo'}) - new_source = db_interface.add_harvest_source({ - 'name': 'Test Source', - 'notification_emails': ['test@example.com'], - 'frequency': 'daily', - 'url': "http://example-3.com", - 'schema_type': 'strict', - 'source_type': 'json' - }, str(org_data.id)) - - job_data = { - 'status': 'in_progress', - 'date_created': '2022-01-01', - 'date_finished': '2022-01-02', - 'records_added': 10, - 'records_updated': 5, - 'records_deleted': 2, - 'records_errored': 1, - 'records_ignored': 0 +@pytest.fixture +def org_data(): + return {'name': 'Test Org', 'logo': 'https://example.com/logo.png'} + +@pytest.fixture +def organization(interface, org_data): + org = interface.add_organization(org_data) + return org + +def test_add_organization(interface, org_data): + org = interface.add_organization(org_data) + assert org is not None + assert org.name == 'Test Org' + +def test_get_all_organizations(interface, org_data): + interface.add_organization(org_data) + + orgs = interface.get_all_organizations() + assert len(orgs) > 0 + assert orgs[0]['name'] == 'Test Org' + +def test_update_organization(interface, organization): + updates = {'name': 'Updated Org'} + updated_org = interface.update_organization(organization.id, updates) + assert updated_org['name'] == 'Updated Org' + +def test_delete_organization(interface, organization): + result = interface.delete_organization(organization.id) + assert result == "Organization deleted successfully" + +@pytest.fixture +def source_data(organization): + return { + "name": "Test Source", + "notification_emails": "email@example.com", + "organization_id": organization.id, + "frequency": "daily", + "url": "http://example.com", + "schema_type": "type1", + "source_type": "typeA", + "status": "active" } - new_job = db_interface.add_harvest_job(job_data, str(new_source.id)) - assert new_job.harvest_source_id == new_source.id + +def test_add_harvest_source(interface, source_data): + source = interface.add_harvest_source(source_data) + assert source is not None + assert source.name == source_data["name"] + +def test_get_all_harvest_sources(interface, source_data): + interface.add_harvest_source(source_data) + sources = interface.get_all_harvest_sources() + assert len(sources) > 0 + assert sources[0]["name"] == source_data["name"] + +def test_get_harvest_source(interface, source_data): + source = interface.add_harvest_source(source_data) + fetched_source = interface.get_harvest_source(source.id) + assert fetched_source is not None + assert fetched_source["name"] == source_data["name"] + +def test_update_harvest_source(interface, source_data): + source = interface.add_harvest_source(source_data) + updates = {"name": "Updated Test Source"} + updated_source = interface.update_harvest_source(source.id, updates) + assert updated_source is not None + assert updated_source["name"] == updates["name"] + +def test_delete_harvest_source(interface, source_data): + source = interface.add_harvest_source(source_data) + assert source is not None + + response = interface.delete_harvest_source(source.id) + assert response == "Harvest source deleted successfully" + + deleted_source = interface.get_harvest_source(source.id) + assert deleted_source is None diff --git a/tests/integration/cf/test_cf_tasks_int.py b/tests/integration/cf/test_cf_tasks_int.py new file mode 100644 index 00000000..10bd437e --- /dev/null +++ b/tests/integration/cf/test_cf_tasks_int.py @@ -0,0 +1,28 @@ +class TestCFTasking: + def test_add_task(self, cf_handler, dhl_cf_task_data): + + assert cf_handler.start_task(**dhl_cf_task_data) is not None + + def test_get_task(self, cf_handler, dhl_cf_task_data): + + task = cf_handler.get_task( + dhl_cf_task_data["app_guuid"], dhl_cf_task_data["task_id"] + ) + assert task is not None + + def test_get_all_app_tasks(self, cf_handler, dhl_cf_task_data): + tasks = cf_handler.get_all_app_tasks(dhl_cf_task_data["app_guuid"]) + assert tasks is not None + + def test_cancel_task(self, cf_handler, dhl_cf_task_data): + + task = cf_handler.stop_task(dhl_cf_task_data["task_id"]) + assert task is not None + + def test_read_recent_task_logs(self, cf_handler, dhl_cf_task_data): + + logs = cf_handler.read_recent_app_logs( + dhl_cf_task_data["app_guuid"], dhl_cf_task_data["task_id"] + ) + + assert logs is not None diff --git a/tests/unit/cf/test_cf_tasks.py b/tests/unit/cf/test_cf_tasks.py new file mode 100644 index 00000000..cf6e507c --- /dev/null +++ b/tests/unit/cf/test_cf_tasks.py @@ -0,0 +1,47 @@ +from unittest.mock import patch +from harvester.utils import CFHandler + + +class TestCFTasking: + @patch.object(CFHandler, "start_task") + def test_add_task(self, start_mock, cf_handler, dhl_cf_task_data): + start_mock.return_value = {"guuid": "test"} + + assert cf_handler.start_task(**dhl_cf_task_data) is not None + + @patch.object(CFHandler, "get_task") + def test_get_task(self, get_mock, cf_handler, dhl_cf_task_data): + get_mock.return_value = {"guuid": "test"} + + task = cf_handler.get_task(dhl_cf_task_data["task_id"]) + assert task is not None + + @patch.object(CFHandler, "get_all_app_tasks") + def test_get_all_app_tasks(self, tasks_mock, cf_handler, dhl_cf_task_data): + tasks_mock.return_value = [{"guuid": "test"}] + + tasks = cf_handler.get_all_app_tasks(dhl_cf_task_data["app_guuid"]) + assert len(tasks) > 0 + + def test_get_all_running_app_tasks(self, cf_handler): + + tasks = [{"state": "RUNNING"}, {"state": "SUCCEEDED"}] + running_tasks = cf_handler.get_all_running_tasks(tasks) + assert running_tasks == 1 + + @patch.object(CFHandler, "stop_task") + def test_cancel_task(self, stop_mock, cf_handler, dhl_cf_task_data): + stop_mock.return_value = {"guuid": "test"} + + task = cf_handler.stop_task(dhl_cf_task_data["task_id"]) + assert task is not None + + @patch.object(CFHandler, "read_recent_app_logs") + def test_read_recent_task_logs(self, read_mock, cf_handler, dhl_cf_task_data): + read_mock.return_value = "recent information on the task" + + logs = cf_handler.read_recent_app_logs( + dhl_cf_task_data["app_guuid"], dhl_cf_task_data["task_id"] + ) + + assert logs is not None diff --git a/tests/unit/exception/test_exception_handling.py b/tests/unit/exception/test_exception_handling.py index 73e361df..c3a4d430 100644 --- a/tests/unit/exception/test_exception_handling.py +++ b/tests/unit/exception/test_exception_handling.py @@ -51,9 +51,8 @@ def test_add_harvest_source(self, db_interface): "records_ignored": 0, } db_interface.add_organization(organization) - db_interface.add_harvest_source(harvest_source, - harvest_source["organization_id"]) - db_interface.add_harvest_job(harvest_job, harvest_job["harvest_source_id"]) + db_interface.add_harvest_source(harvest_source) + db_interface.add_harvest_job(harvest_job) def test_bad_harvest_source_url_exception(self, bad_url_dcatus_config): harvest_source = HarvestSource(**bad_url_dcatus_config) From 18974f7c090f14c0eaaafcf8afc8f201c675dd4e Mon Sep 17 00:00:00 2001 From: Jin-Sun-tts Date: Tue, 9 Apr 2024 13:11:58 -0400 Subject: [PATCH 3/3] remove some test : --- app/readme.txt | 61 +++++++++----------------------------------------- 1 file changed, 10 insertions(+), 51 deletions(-) diff --git a/app/readme.txt b/app/readme.txt index 283eb9a6..410ec309 100644 --- a/app/readme.txt +++ b/app/readme.txt @@ -28,14 +28,15 @@ DATAGOV-HARVESTING-LOGIC └── run.py +examples: -curl -X POST http://localhost:8080/organization/add -H "Content-Type: application/json" -d ' +curl -X POST http://{site}/organization/add -H "Content-Type: application/json" -d ' { "name": "New Org", "logo": "test url" }' -curl -X POST http://localhost:8080/harvest_source/add -H "Content-Type: application/json" -d ' +curl -X POST http://{site}/harvest_source/add -H "Content-Type: application/json" -d ' { "organization_id": "4ed9d20a-7de8-4c2d-884f-86b50ec8065d", "name": "Example Harvest Source", @@ -47,20 +48,20 @@ curl -X POST http://localhost:8080/harvest_source/add -H "Content-Type: applicat } ' -curl -X POST http://localhost:8080/harvest_job/add -H "Content-Type: application/json" -d ' +curl -X POST http://{site}/harvest_job/add -H "Content-Type: application/json" -d ' { "harvest_source_id": "59e93b86-83f1-4b70-afa7-c7ca027aeacb", "status": "in_progress" }' -curl -X POST http://localhost:8080/harvest_record/add -H "Content-Type: application/json" -d ' +curl -X POST http://{site}/harvest_record/add -H "Content-Type: application/json" -d ' { "id": "identifier-1", "harvest_job_id": "a8c03b83-907c-41c9-95aa-d71c3be626b1", "harvest_source_id": "59e93b86-83f1-4b70-afa7-c7ca027aeacb" }' -curl -X POST http://localhost:8080/harvest_error/add -H "Content-Type: application/json" -d ' +curl -X POST http://{site}/harvest_error/add -H "Content-Type: application/json" -d ' { "harvest_job_id": "a8c03b83-907c-41c9-95aa-d71c3be626b1", "harvest_record_id": "identifier-1", @@ -70,17 +71,14 @@ curl -X POST http://localhost:8080/harvest_error/add -H "Content-Type: applicati } ' -curl -X GET http://localhost:8080/harvest_job/a8c03b83-907c-41c9-95aa-d71c3be626b1 +curl -X GET http://{site}/harvest_job/a8c03b83-907c-41c9-95aa-d71c3be626b1 +curl -X DELETE http://{site}/organization/da183992-e598-467a-b245-a3fe8ee2fb91 +curl -X DELETE http://{site}/harvest_source/ca299fd6-5553-401e-ac36-05b841e31cd1 -curl -X DELETE http://localhost:8080/organization/da183992-e598-467a-b245-a3fe8ee2fb91 - -curl -X DELETE http://localhost:8080/harvest_source/c7abedad-4420-4d71-b519-3284f9a9a132 - - -curl -X PUT http://localhost:8080/harvest_job/c82e0481-5884-4029-931e-234c53767e50 -H "Content-Type: application/json" -d ' +curl -X PUT http://{site}/harvest_job/c82e0481-5884-4029-931e-234c53767e50 -H "Content-Type: application/json" -d ' { "status": "complete", "date_finished": "Wed, 27 Mar 2024 22:37:52 GMT", @@ -90,42 +88,3 @@ curl -X PUT http://localhost:8080/harvest_job/c82e0481-5884-4029-931e-234c53767e "records_errored": 4, "records_ignored": 2 }' - -------------- - -curl -X POST https://harvester-dev-datagov.app.cloud.gov/organization/add -H "Content-Type: application/json" -d ' -{ - "name": "New Org 1", - "logo": "test url for new org1" -}' - - -curl -X POST https://lharvester-dev-datagov.app.cloud.gov/harvest_job/add -H "Content-Type: application/json" -d ' -{ - "harvest_source_id": "760129d6-2bf0-4c94-94b9-09622a8a0b23", - "status": "in_progress", - "date_created": "Wed, 27 Mar 2024 20:37:52 GMT" -}' - -curl -X PUT https://harvester-dev-datagov.app.cloud.gov/harvest_job/ -H "Content-Type: application/json" -d ' -{ - "status": "complete", - "date_finished": "Wed, 27 Mar 2024 22:37:52 GMT", - "records_added": 200, - "records_updated": 50, - "records_deleted": 6, - "records_errored": 4, - "records_ignored": 2 -}' - - -curl -X PUT https://harvester-dev-datagov.app.cloud.gov/organization/4c456ed3-4717-4933-82c9-d87464063f19 -H "Content-Type: application/json" -d ' -{ - "logo": "url for test 1" -}' - - -curl -X DELETE https://harvester-dev-datagov.app.cloud.gov/organization/e1301d69-d747-4040-9e31-bba7c9508fb9 - - -curl -X DELETE https://harvester-dev-datagov.app.cloud.gov/organization/4c456ed3-4717-4933-82c9-d87464063f19 \ No newline at end of file