From 88fd8f23279dd0eca924b833bf8fc81d5c8141e1 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Mon, 26 Feb 2024 16:24:02 +0100 Subject: [PATCH 01/14] timestamp added to handling_incidents --- app/api/routes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/api/routes.py b/app/api/routes.py index d7810c4..0069bfd 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -98,6 +98,7 @@ def handling_incidents( ) update_incident_status( dst_incident, + f"{datetime.now()}: " f"{comp_with_attrs} moved from {src_incident.text}" ) src_incident.end_date = datetime.now() @@ -117,7 +118,8 @@ def handling_incidents( update_incident_status( src_incident, ( - f"Impact changed: from {impacts[src_incident.impact].key} " + f"{datetime.now()}: " + f"impact changed from {impacts[src_incident.impact].key} " f"to {impacts[impact].key}" ) ) @@ -135,6 +137,7 @@ def handling_incidents( ) update_incident_status( dst_incident, + f"{datetime.now()}: " f"{comp_with_attrs} moved from {src_incident.text}" ) src_incident.components.remove(target_component) From 6df0e031c72728149b60f7a3727ee94855f09639 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Mon, 18 Mar 2024 16:27:34 +0100 Subject: [PATCH 02/14] aware datetime objects with UTC timezone --- app/api/routes.py | 47 ++++++++++++++++++++-------------- app/models.py | 49 ++++++++++++++++++++---------------- app/tests/unit/test_api.py | 6 ++--- app/tests/unit/test_model.py | 4 +-- app/tests/unit/test_rss.py | 8 +++--- app/tests/unit/test_web.py | 4 +-- app/web/routes.py | 15 ++++++++--- 7 files changed, 77 insertions(+), 56 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index 0069bfd..0d29ec5 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -from datetime import datetime +from datetime import datetime, timezone from app import authorization from app import cache @@ -42,10 +42,30 @@ def inc_by_impact(incidents, impact): return incident_match -def add_component_to_incident(target_component, incident): +def update_incident_status(incident, text_status, status="SYSTEM"): + update = IncidentStatus( + incident_id=incident.id, + text=text_status, + status=status, + ) + current_app.logger.debug(f"UPDATE_STATUS: {text_status}") + db.session.add(update) + + +def add_component_to_incident( + target_component, + incident, + comp_with_attrs=None +): current_app.logger.debug( f"Add {target_component} to the incident: {incident}" ) + update_incident_status( + incident, + ( + f"{comp_with_attrs} added to {incident.text}" + ) + ) incident.components.append(target_component) db.session.commit() return incident @@ -55,7 +75,7 @@ def create_new_incident(target_component, impact, text): new_incident = Incident( text=text, impact=impact, - start_date=datetime.now(), + start_date=datetime.now(timezone.utc), components=[target_component], system=True, ) @@ -64,16 +84,6 @@ def create_new_incident(target_component, impact, text): return new_incident -def update_incident_status(incident, text_status, status="SYSTEM"): - update = IncidentStatus( - incident_id=incident.id, - text=text_status, - status=status, - ) - current_app.logger.debug(f"UPDATE_STATUS: {text_status}") - db.session.add(update) - - def handling_incidents( target_component, impact, @@ -98,10 +108,11 @@ def handling_incidents( ) update_incident_status( dst_incident, - f"{datetime.now()}: " - f"{comp_with_attrs} moved from {src_incident.text}" + ( + f"{comp_with_attrs} moved from {src_incident.text}" + ) ) - src_incident.end_date = datetime.now() + src_incident.end_date = datetime.now(timezone.utc) dst_incident.components.append(target_component) db.session.commit() return dst_incident @@ -118,7 +129,6 @@ def handling_incidents( update_incident_status( src_incident, ( - f"{datetime.now()}: " f"impact changed from {impacts[src_incident.impact].key} " f"to {impacts[impact].key}" ) @@ -137,7 +147,6 @@ def handling_incidents( ) update_incident_status( dst_incident, - f"{datetime.now()}: " f"{comp_with_attrs} moved from {src_incident.text}" ) src_incident.components.remove(target_component) @@ -340,7 +349,7 @@ def post(self, data): incident_match = inc_by_impact(incidents, impact) if incident_match: return add_component_to_incident( - target_component, incident_match + target_component, incident_match, comp_with_attrs ) else: current_app.logger.debug( diff --git a/app/models.py b/app/models.py index 20fe79f..def0531 100644 --- a/app/models.py +++ b/app/models.py @@ -11,7 +11,7 @@ # under the License. # -import datetime +from datetime import datetime, timezone from typing import List from app import db @@ -108,9 +108,9 @@ def all_with_active_incidents(): PropComparator.and_( or_( Incident.end_date.is_(None), - Incident.end_date > datetime.datetime.now() + Incident.end_date > datetime.now(timezone.utc) ), - Incident.start_date <= datetime.datetime.now(), + Incident.start_date <= datetime.now(timezone.utc), ), ), ) @@ -171,9 +171,9 @@ def find_by_name_and_attributes(name, attributes): def calculate_sla(self): """Calculate component availability on the month basis""" - time_now = datetime.datetime.now() - this_month_start = datetime.datetime(time_now.year, time_now.month, 1) - + time_now = datetime.now(timezone.utc) + this_month_start = datetime( + time_now.year, time_now.month, 1, tzinfo=timezone.utc) outages = [inc for inc in self.incidents if inc.impact == 3 and inc.end_date is not None] outages_dict = Incident.get_history_by_months(outages) @@ -202,12 +202,14 @@ def calculate_sla(self): for outage in outage_group: outage_start = outage.start_date + if outage_start < month_start: diff = month_start - outage_start prev_month_minutes += diff.total_seconds() / 60 outage_start = month_start - diff = outage.end_date - outage_start + outage_end = outage.end_date + diff = outage_end - outage_start outages_minutes += diff.total_seconds() / 60 sla_dict[month_start] = ( @@ -270,10 +272,14 @@ class Incident(Base): __tablename__ = "incident" id = mapped_column(Integer, primary_key=True, index=True) text: Mapped[str] = mapped_column(String()) - start_date: Mapped[datetime.datetime] = mapped_column( + start_date: Mapped[datetime] = mapped_column( + db.DateTime(timezone=True), insert_default=func.now() ) - end_date: Mapped[datetime.datetime] = mapped_column(nullable=True) + end_date: Mapped[datetime] = mapped_column( + db.DateTime(timezone=True), + nullable=True + ) impact: Mapped[int] = mapped_column(db.SmallInteger) # upgrade: system: Mapped[bool] = mapped_column(Boolean, default=False) system: Mapped[bool] = mapped_column(Boolean, default=False) @@ -297,9 +303,9 @@ def get_all_active(): select(Incident).filter( or_( Incident.end_date.is_(None), - Incident.end_date > datetime.datetime.now() + Incident.end_date > datetime.now(timezone.utc) ), - Incident.start_date <= datetime.datetime.now(), + Incident.start_date <= datetime.now(timezone.utc), ) ).all() @@ -309,7 +315,7 @@ def get_all_closed(): return db.session.scalars( select(Incident).filter( Incident.end_date.is_not(None), - Incident.end_date < datetime.datetime.now() + Incident.end_date < datetime.now(timezone.utc) ) ).all() @@ -320,10 +326,11 @@ def get_history_by_months(incident_list): incident_dict = {} for incident in incident_list: incident_dict.setdefault( - datetime.datetime( + datetime( incident.end_date.year, incident.end_date.month, - 1), + 1, + tzinfo=timezone.utc), [] ).append(incident) return incident_dict @@ -337,11 +344,11 @@ def get_active_maintenance(): return db.session.scalars( select(Incident).filter( # already started - Incident.start_date <= datetime.datetime.now(), + Incident.start_date <= datetime.now(timezone.utc), # not closed or_( Incident.end_date.is_(None), - Incident.end_date > datetime.datetime.now() + Incident.end_date > datetime.now(timezone.utc) ), Incident.impact == 0, ) @@ -352,7 +359,7 @@ def get_planned_maintenances(): """Return planned maintenances""" return db.session.scalars( select(Incident).filter( - Incident.start_date > datetime.datetime.now(), + Incident.start_date > datetime.now(timezone.utc), Incident.impact == 0, ) ).all() @@ -365,7 +372,7 @@ def get_active_m(): return db.session.scalars( select(Incident).filter( # already started - Incident.start_date <= datetime.datetime.now(), + Incident.start_date <= datetime.now(timezone.utc), # not closed Incident.end_date.is_(None), Incident.impact != 0, @@ -381,7 +388,7 @@ def get_active(): return db.session.scalars( select(Incident).filter( # already started - Incident.start_date <= datetime.datetime.now(), + Incident.start_date <= datetime.now(timezone.utc), # not closed Incident.end_date.is_(None), Incident.impact != 0, @@ -441,8 +448,8 @@ class IncidentStatus(Base): id = mapped_column(Integer, primary_key=True, index=True) incident_id = mapped_column(ForeignKey("incident.id"), index=True) incident: Mapped["Incident"] = relationship(back_populates="updates") - timestamp: Mapped[datetime.datetime] = mapped_column( - db.DateTime, insert_default=func.now() + timestamp: Mapped[datetime] = mapped_column( + db.DateTime(timezone=True), insert_default=func.now() ) text: Mapped[str] = mapped_column(String()) status: Mapped[str] = mapped_column(String()) diff --git a/app/tests/unit/test_api.py b/app/tests/unit/test_api.py index 5d97d20..dc3d916 100644 --- a/app/tests/unit/test_api.py +++ b/app/tests/unit/test_api.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. # -import datetime import json +from datetime import datetime, timedelta, timezone from unittest import TestCase @@ -74,8 +74,8 @@ def setUp(self): text="inc", components=[comp1], impact=1, - end_date=datetime.datetime.now() - - datetime.timedelta(days=1), + end_date=datetime.now(timezone.utc) + - timedelta(days=1), ) ) db.session.commit() diff --git a/app/tests/unit/test_model.py b/app/tests/unit/test_model.py index 88ac4a7..fbd8a02 100644 --- a/app/tests/unit/test_model.py +++ b/app/tests/unit/test_model.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -import datetime +from datetime import datetime, timedelta, timezone from unittest import TestCase from app import create_app @@ -118,7 +118,7 @@ def setUp(self): i2 = Incident( text="Incident2", impact="1", - end_date=datetime.datetime.now() - datetime.timedelta(days=1), + end_date=datetime.now(timezone.utc) - timedelta(days=1), components=[comp1], ) db.session.add(i2) diff --git a/app/tests/unit/test_rss.py b/app/tests/unit/test_rss.py index 056f6cd..60d5680 100644 --- a/app/tests/unit/test_rss.py +++ b/app/tests/unit/test_rss.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -import datetime +from datetime import datetime, timedelta, timezone from unittest import TestCase @@ -60,8 +60,7 @@ def setUp(self): text="inc", components=[comp1], impact=1, - end_date=datetime.datetime.now() - - datetime.timedelta(days=1), + end_date=datetime.now(timezone.utc) - timedelta(days=1), ) ) db.session.add( @@ -69,8 +68,7 @@ def setUp(self): text="inc2", components=[comp2], impact=2, - end_date=datetime.datetime.now() - - datetime.timedelta(days=2), + end_date=datetime.now(timezone.utc) - timedelta(days=2), ) ) db.session.commit() diff --git a/app/tests/unit/test_web.py b/app/tests/unit/test_web.py index 7fec0df..724b1f3 100644 --- a/app/tests/unit/test_web.py +++ b/app/tests/unit/test_web.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -import datetime +from datetime import datetime, timedelta, timezone from unittest import TestCase from app import create_app @@ -62,7 +62,7 @@ def setUp(self): text="inc", components=[comp1], impact=1, - end_date=datetime.datetime.now() - datetime.timedelta(days=1), + end_date=datetime.now(timezone.utc) - timedelta(days=1), ) db.session.add(inc1) db.session.commit() diff --git a/app/web/routes.py b/app/web/routes.py index 632f124..d90faa6 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -from datetime import datetime +from datetime import datetime, timezone from app import authorization from app import cache @@ -36,6 +36,12 @@ from flask import url_for +def get_utc_timestamp(): + """Return current UTC timestamp string compatible with PostgreSQL""" + current_time_utc = datetime.now(timezone.utc) + return current_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f%z') + + @bp.route("/", methods=["GET"]) @bp.route("/index", methods=["GET"]) @cache.cached(unless=lambda: "user" in session, @@ -136,7 +142,7 @@ def new_incident(current_user): inc.components.remove(comp) else: messages_to.append("Incident closed by system") - inc.end_date = datetime.now() + inc.end_date = get_utc_timestamp() if messages_to: update_incident(inc, ', '.join(messages_to)) if messages_from: @@ -184,7 +190,7 @@ def incident(incident_id): if new_status in ["completed", "resolved"]: # Incident is completed new_impact = incident.impact - incident.end_date = datetime.now() + incident.end_date = get_utc_timestamp() current_app.logger.debug( f"{incident} closed by {get_user_string(session['user'])}" ) @@ -248,7 +254,8 @@ def history(): timeout=300, ) def sla(): - time_now = datetime.now() + time_now_str = get_utc_timestamp() + time_now = datetime.strptime(time_now_str, '%Y-%m-%d %H:%M:%S.%f%z') months = [time_now + relativedelta(months=-mon) for mon in range(6)] return render_template( From baaf747006c4b93a4540eb428ce23ab79a63012b Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Mon, 18 Mar 2024 17:32:56 +0100 Subject: [PATCH 03/14] database migration --- ...076c8bbfe_using_aware_datetime_timezone.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py diff --git a/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py b/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py new file mode 100644 index 0000000..3758a50 --- /dev/null +++ b/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py @@ -0,0 +1,66 @@ +"""using aware datetime(timezone) + +Revision ID: bab076c8bbfe +Revises: 14621c95e3ee +Create Date: 2024-03-18 17:11:10.434492 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'bab076c8bbfe' +down_revision = '14621c95e3ee' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('incident', schema=None) as batch_op: + batch_op.alter_column('start_date', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=False) + batch_op.alter_column('end_date', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=True) + + with op.batch_alter_table('incident_component_relation', schema=None) as batch_op: + batch_op.create_foreign_key(None, 'incident', ['incident_id'], ['id']) + + with op.batch_alter_table('incident_status', schema=None) as batch_op: + batch_op.alter_column('timestamp', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=False) + batch_op.create_foreign_key(None, 'incident', ['incident_id'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('incident_status', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.alter_column('timestamp', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=False) + + with op.batch_alter_table('incident_component_relation', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + + with op.batch_alter_table('incident', schema=None) as batch_op: + batch_op.alter_column('end_date', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=True) + batch_op.alter_column('start_date', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=False) + + # ### end Alembic commands ### From 7452dd247c57a3c540610279abb1ccc2777a6af5 Mon Sep 17 00:00:00 2001 From: Olha Kashyrina Date: Tue, 19 Mar 2024 18:02:08 +0200 Subject: [PATCH 04/14] fix db migration --- .../bab076c8bbfe_using_aware_datetime_timezone.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py b/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py index 3758a50..8631d97 100644 --- a/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py +++ b/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py @@ -29,14 +29,14 @@ def upgrade(): existing_nullable=True) with op.batch_alter_table('incident_component_relation', schema=None) as batch_op: - batch_op.create_foreign_key(None, 'incident', ['incident_id'], ['id']) + batch_op.create_foreign_key('fk_incident_component', 'incident', ['incident_id'], ['id']) with op.batch_alter_table('incident_status', schema=None) as batch_op: batch_op.alter_column('timestamp', existing_type=postgresql.TIMESTAMP(), type_=sa.DateTime(timezone=True), existing_nullable=False) - batch_op.create_foreign_key(None, 'incident', ['incident_id'], ['id']) + batch_op.create_foreign_key('fk_incident_status', 'incident', ['incident_id'], ['id']) # ### end Alembic commands ### @@ -44,14 +44,14 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table('incident_status', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint('fk_incident_status', type_='foreignkey') batch_op.alter_column('timestamp', existing_type=sa.DateTime(timezone=True), type_=postgresql.TIMESTAMP(), existing_nullable=False) with op.batch_alter_table('incident_component_relation', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint('fk_incident_component', type_='foreignkey') with op.batch_alter_table('incident', schema=None) as batch_op: batch_op.alter_column('end_date', From 9ee4b59bac6d1c82f0328933a3120b393f4258c6 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Fri, 22 Mar 2024 16:40:28 +0100 Subject: [PATCH 05/14] cleaned up --- app/web/routes.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/web/routes.py b/app/web/routes.py index d90faa6..e149cda 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -36,12 +36,6 @@ from flask import url_for -def get_utc_timestamp(): - """Return current UTC timestamp string compatible with PostgreSQL""" - current_time_utc = datetime.now(timezone.utc) - return current_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f%z') - - @bp.route("/", methods=["GET"]) @bp.route("/index", methods=["GET"]) @cache.cached(unless=lambda: "user" in session, @@ -142,7 +136,7 @@ def new_incident(current_user): inc.components.remove(comp) else: messages_to.append("Incident closed by system") - inc.end_date = get_utc_timestamp() + inc.end_date = datetime.now(timezone.utc) if messages_to: update_incident(inc, ', '.join(messages_to)) if messages_from: @@ -190,7 +184,7 @@ def incident(incident_id): if new_status in ["completed", "resolved"]: # Incident is completed new_impact = incident.impact - incident.end_date = get_utc_timestamp() + incident.end_date = datetime.now(timezone.utc) current_app.logger.debug( f"{incident} closed by {get_user_string(session['user'])}" ) @@ -254,8 +248,7 @@ def history(): timeout=300, ) def sla(): - time_now_str = get_utc_timestamp() - time_now = datetime.strptime(time_now_str, '%Y-%m-%d %H:%M:%S.%f%z') + time_now = datetime.now(timezone.utc) months = [time_now + relativedelta(months=-mon) for mon in range(6)] return render_template( From 67294b4bcc596946f5b459f8c9e21bb53eeb54be Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Tue, 30 Apr 2024 15:10:57 +0200 Subject: [PATCH 06/14] revert changes in specific files --- app/models.py | 49 ++++++++++++++++-------------------- app/tests/unit/test_api.py | 6 ++--- app/tests/unit/test_model.py | 4 +-- app/tests/unit/test_rss.py | 8 +++--- app/tests/unit/test_web.py | 4 +-- 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/app/models.py b/app/models.py index e2d374c..402c990 100644 --- a/app/models.py +++ b/app/models.py @@ -11,7 +11,7 @@ # under the License. # -from datetime import datetime, timezone +import datetime from typing import List from app import db @@ -108,9 +108,9 @@ def all_with_active_incidents(): PropComparator.and_( or_( Incident.end_date.is_(None), - Incident.end_date > datetime.now(timezone.utc) + Incident.end_date > datetime.datetime.now() ), - Incident.start_date <= datetime.now(timezone.utc), + Incident.start_date <= datetime.datetime.now(), ), ), ) @@ -171,9 +171,9 @@ def find_by_name_and_attributes(name, attributes): def calculate_sla(self): """Calculate component availability on the month basis""" - time_now = datetime.now(timezone.utc) - this_month_start = datetime( - time_now.year, time_now.month, 1, tzinfo=timezone.utc) + time_now = datetime.datetime.now() + this_month_start = datetime.datetime(time_now.year, time_now.month, 1) + outages = [inc for inc in self.incidents if inc.impact == 3 and inc.end_date is not None] outages_dict = Incident.get_history_by_months(outages) @@ -202,14 +202,12 @@ def calculate_sla(self): for outage in outage_group: outage_start = outage.start_date - if outage_start < month_start: diff = month_start - outage_start prev_month_minutes += diff.total_seconds() / 60 outage_start = month_start - outage_end = outage.end_date - diff = outage_end - outage_start + diff = outage.end_date - outage_start outages_minutes += diff.total_seconds() / 60 sla_dict[month_start] = ( @@ -272,14 +270,10 @@ class Incident(Base): __tablename__ = "incident" id = mapped_column(Integer, primary_key=True, index=True) text: Mapped[str] = mapped_column(String()) - start_date: Mapped[datetime] = mapped_column( - db.DateTime(timezone=True), + start_date: Mapped[datetime.datetime] = mapped_column( insert_default=func.now() ) - end_date: Mapped[datetime] = mapped_column( - db.DateTime(timezone=True), - nullable=True - ) + end_date: Mapped[datetime.datetime] = mapped_column(nullable=True) impact: Mapped[int] = mapped_column(db.SmallInteger) # upgrade: system: Mapped[bool] = mapped_column(Boolean, default=False) system: Mapped[bool] = mapped_column(Boolean, default=False) @@ -303,9 +297,9 @@ def get_all_active(): select(Incident).filter( or_( Incident.end_date.is_(None), - Incident.end_date > datetime.now(timezone.utc) + Incident.end_date > datetime.datetime.now() ), - Incident.start_date <= datetime.now(timezone.utc), + Incident.start_date <= datetime.datetime.now(), ) ).all() @@ -315,7 +309,7 @@ def get_all_closed(): return db.session.scalars( select(Incident).filter( Incident.end_date.is_not(None), - Incident.end_date < datetime.now(timezone.utc) + Incident.end_date < datetime.datetime.now() ) ).all() @@ -326,11 +320,10 @@ def get_history_by_months(incident_list): incident_dict = {} for incident in incident_list: incident_dict.setdefault( - datetime( + datetime.datetime( incident.end_date.year, incident.end_date.month, - 1, - tzinfo=timezone.utc), + 1), [] ).append(incident) return incident_dict @@ -344,11 +337,11 @@ def get_active_maintenance(): return db.session.scalars( select(Incident).filter( # already started - Incident.start_date <= datetime.now(timezone.utc), + Incident.start_date <= datetime.datetime.now(), # not closed or_( Incident.end_date.is_(None), - Incident.end_date > datetime.now(timezone.utc) + Incident.end_date > datetime.datetime.now() ), Incident.impact == 0, ) @@ -359,7 +352,7 @@ def get_planned_maintenances(): """Return planned maintenances""" return db.session.scalars( select(Incident).filter( - Incident.start_date > datetime.now(timezone.utc), + Incident.start_date > datetime.datetime.now(), Incident.impact == 0, ) ).all() @@ -372,7 +365,7 @@ def get_active_m(): return db.session.scalars( select(Incident).filter( # already started - Incident.start_date <= datetime.now(timezone.utc), + Incident.start_date <= datetime.datetime.now(), # not closed Incident.end_date.is_(None), Incident.impact != 0, @@ -388,7 +381,7 @@ def get_active(): return db.session.scalars( select(Incident).filter( # already started - Incident.start_date <= datetime.now(timezone.utc), + Incident.start_date <= datetime.datetime.now(), # not closed Incident.end_date.is_(None), Incident.impact != 0, @@ -448,8 +441,8 @@ class IncidentStatus(Base): id = mapped_column(Integer, primary_key=True, index=True) incident_id = mapped_column(ForeignKey("incident.id"), index=True) incident: Mapped["Incident"] = relationship(back_populates="updates") - timestamp: Mapped[datetime] = mapped_column( - db.DateTime(timezone=True), insert_default=func.now() + timestamp: Mapped[datetime.datetime] = mapped_column( + db.DateTime, insert_default=func.now() ) text: Mapped[str] = mapped_column(String()) status: Mapped[str] = mapped_column(String()) diff --git a/app/tests/unit/test_api.py b/app/tests/unit/test_api.py index dc3d916..5d97d20 100644 --- a/app/tests/unit/test_api.py +++ b/app/tests/unit/test_api.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. # +import datetime import json -from datetime import datetime, timedelta, timezone from unittest import TestCase @@ -74,8 +74,8 @@ def setUp(self): text="inc", components=[comp1], impact=1, - end_date=datetime.now(timezone.utc) - - timedelta(days=1), + end_date=datetime.datetime.now() + - datetime.timedelta(days=1), ) ) db.session.commit() diff --git a/app/tests/unit/test_model.py b/app/tests/unit/test_model.py index fbd8a02..88ac4a7 100644 --- a/app/tests/unit/test_model.py +++ b/app/tests/unit/test_model.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -from datetime import datetime, timedelta, timezone +import datetime from unittest import TestCase from app import create_app @@ -118,7 +118,7 @@ def setUp(self): i2 = Incident( text="Incident2", impact="1", - end_date=datetime.now(timezone.utc) - timedelta(days=1), + end_date=datetime.datetime.now() - datetime.timedelta(days=1), components=[comp1], ) db.session.add(i2) diff --git a/app/tests/unit/test_rss.py b/app/tests/unit/test_rss.py index 60d5680..056f6cd 100644 --- a/app/tests/unit/test_rss.py +++ b/app/tests/unit/test_rss.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -from datetime import datetime, timedelta, timezone +import datetime from unittest import TestCase @@ -60,7 +60,8 @@ def setUp(self): text="inc", components=[comp1], impact=1, - end_date=datetime.now(timezone.utc) - timedelta(days=1), + end_date=datetime.datetime.now() + - datetime.timedelta(days=1), ) ) db.session.add( @@ -68,7 +69,8 @@ def setUp(self): text="inc2", components=[comp2], impact=2, - end_date=datetime.now(timezone.utc) - timedelta(days=2), + end_date=datetime.datetime.now() + - datetime.timedelta(days=2), ) ) db.session.commit() diff --git a/app/tests/unit/test_web.py b/app/tests/unit/test_web.py index 724b1f3..7fec0df 100644 --- a/app/tests/unit/test_web.py +++ b/app/tests/unit/test_web.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -from datetime import datetime, timedelta, timezone +import datetime from unittest import TestCase from app import create_app @@ -62,7 +62,7 @@ def setUp(self): text="inc", components=[comp1], impact=1, - end_date=datetime.now(timezone.utc) - timedelta(days=1), + end_date=datetime.datetime.now() - datetime.timedelta(days=1), ) db.session.add(inc1) db.session.commit() From e1ac58c662f78c80714d712efadf752be3a1bb47 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Tue, 30 Apr 2024 15:12:03 +0200 Subject: [PATCH 07/14] time zone changes removed --- app/api/routes.py | 2 +- ...076c8bbfe_using_aware_datetime_timezone.py | 66 ------------------- 2 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py diff --git a/app/api/routes.py b/app/api/routes.py index f686c04..b2a0926 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -from datetime import datetime, timezone +from datetime import datetime from app import authorization from app import cache diff --git a/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py b/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py deleted file mode 100644 index 8631d97..0000000 --- a/migrations/versions/bab076c8bbfe_using_aware_datetime_timezone.py +++ /dev/null @@ -1,66 +0,0 @@ -"""using aware datetime(timezone) - -Revision ID: bab076c8bbfe -Revises: 14621c95e3ee -Create Date: 2024-03-18 17:11:10.434492 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = 'bab076c8bbfe' -down_revision = '14621c95e3ee' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('incident', schema=None) as batch_op: - batch_op.alter_column('start_date', - existing_type=postgresql.TIMESTAMP(), - type_=sa.DateTime(timezone=True), - existing_nullable=False) - batch_op.alter_column('end_date', - existing_type=postgresql.TIMESTAMP(), - type_=sa.DateTime(timezone=True), - existing_nullable=True) - - with op.batch_alter_table('incident_component_relation', schema=None) as batch_op: - batch_op.create_foreign_key('fk_incident_component', 'incident', ['incident_id'], ['id']) - - with op.batch_alter_table('incident_status', schema=None) as batch_op: - batch_op.alter_column('timestamp', - existing_type=postgresql.TIMESTAMP(), - type_=sa.DateTime(timezone=True), - existing_nullable=False) - batch_op.create_foreign_key('fk_incident_status', 'incident', ['incident_id'], ['id']) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('incident_status', schema=None) as batch_op: - batch_op.drop_constraint('fk_incident_status', type_='foreignkey') - batch_op.alter_column('timestamp', - existing_type=sa.DateTime(timezone=True), - type_=postgresql.TIMESTAMP(), - existing_nullable=False) - - with op.batch_alter_table('incident_component_relation', schema=None) as batch_op: - batch_op.drop_constraint('fk_incident_component', type_='foreignkey') - - with op.batch_alter_table('incident', schema=None) as batch_op: - batch_op.alter_column('end_date', - existing_type=sa.DateTime(timezone=True), - type_=postgresql.TIMESTAMP(), - existing_nullable=True) - batch_op.alter_column('start_date', - existing_type=sa.DateTime(timezone=True), - type_=postgresql.TIMESTAMP(), - existing_nullable=False) - - # ### end Alembic commands ### From ce2ee003ba29313487163a0149cc2c7d0a5d1adc Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Tue, 30 Apr 2024 15:20:03 +0200 Subject: [PATCH 08/14] revert web/routes.py to main --- app/web/routes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/web/routes.py b/app/web/routes.py index e149cda..632f124 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # -from datetime import datetime, timezone +from datetime import datetime from app import authorization from app import cache @@ -136,7 +136,7 @@ def new_incident(current_user): inc.components.remove(comp) else: messages_to.append("Incident closed by system") - inc.end_date = datetime.now(timezone.utc) + inc.end_date = datetime.now() if messages_to: update_incident(inc, ', '.join(messages_to)) if messages_from: @@ -184,7 +184,7 @@ def incident(incident_id): if new_status in ["completed", "resolved"]: # Incident is completed new_impact = incident.impact - incident.end_date = datetime.now(timezone.utc) + incident.end_date = datetime.now() current_app.logger.debug( f"{incident} closed by {get_user_string(session['user'])}" ) @@ -248,7 +248,7 @@ def history(): timeout=300, ) def sla(): - time_now = datetime.now(timezone.utc) + time_now = datetime.now() months = [time_now + relativedelta(months=-mon) for mon in range(6)] return render_template( From 29e1c5ba9da6a63a84f68dd7c47d8bbdd324cf09 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Tue, 30 Apr 2024 15:40:35 +0200 Subject: [PATCH 09/14] hyperlinks added --- app/api/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index b2a0926..2eb35f4 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -102,7 +102,7 @@ def handling_incidents( update_incident_status( src_incident, ( - f"{comp_with_attrs} moved to: '{dst_incident.text}', " + f"{comp_with_attrs} moved to: {dst_incident.text}, " "incident closed by system" ) ) @@ -143,7 +143,7 @@ def handling_incidents( ) update_incident_status( src_incident, - f"{comp_with_attrs} moved to {dst_incident.text}" + f"{comp_with_attrs} moved to {dst_incident.text}" ) update_incident_status( dst_incident, From 5b863b8822dd32be1e3fb8fb1463c85ca7528183 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Tue, 30 Apr 2024 16:28:30 +0200 Subject: [PATCH 10/14] hyperlinks fixed --- app/api/routes.py | 44 +++++++++++++++++---------------- app/web/templates/incident.html | 2 +- app/web/templates/index.html | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index 2eb35f4..00fab38 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -27,6 +27,7 @@ from flask import current_app from flask import jsonify from flask import request +from flask import url_for from flask.views import MethodView from flask_smorest import abort @@ -99,19 +100,17 @@ def handling_incidents( f"moved to: '{dst_incident.text}'" ) current_app.logger.debug(f"{src_incident.text} CLOSED") - update_incident_status( - src_incident, - ( - f"{comp_with_attrs} moved to: {dst_incident.text}, " - "incident closed by system" - ) - ) - update_incident_status( - dst_incident, - ( - f"{comp_with_attrs} moved from {src_incident.text}" - ) - ) + + url_d = url_for('web.incident', incident_id=dst_incident.id) + url_s = url_for('web.incident', incident_id=src_incident.id) + link_s = f"{dst_incident.text}" + link_d = f"{src_incident.text}" + update_s = f"{comp_with_attrs} moved to {link_s}, closed by system" + update_d = f"{comp_with_attrs} moved from {link_d}" + + update_incident_status(src_incident, update_s) + update_incident_status(dst_incident, update_d) + src_incident.end_date = datetime.utcnow() dst_incident.components.append(target_component) db.session.commit() @@ -141,14 +140,17 @@ def handling_incidents( f"{target_component} moved from {src_incident.text} to " f"{dst_incident.text}" ) - update_incident_status( - src_incident, - f"{comp_with_attrs} moved to {dst_incident.text}" - ) - update_incident_status( - dst_incident, - f"{comp_with_attrs} moved from {src_incident.text}" - ) + + url_d = url_for('web.incident', incident_id=dst_incident.id) + url_s = url_for('web.incident', incident_id=src_incident.id) + link_s = f"{dst_incident.text}" + link_d = f"{src_incident.text}" + update_s = f"{comp_with_attrs} moved to {link_s}" + update_d = f"{comp_with_attrs} moved from {link_d}" + + update_incident_status(src_incident, update_s) + update_incident_status(dst_incident, update_d) + src_incident.components.remove(target_component) dst_incident.components.append(target_component) db.session.commit() diff --git a/app/web/templates/incident.html b/app/web/templates/incident.html index decc118..0e667bb 100644 --- a/app/web/templates/incident.html +++ b/app/web/templates/incident.html @@ -81,7 +81,7 @@
{{ update.status }}
{{ update.timestamp.strftime('%Y-%m-%d %H:%M') }} UTC
- {{ update.text }} + {{ update.text|safe }}

diff --git a/app/web/templates/index.html b/app/web/templates/index.html index 0a2ac0b..5a1da9a 100644 --- a/app/web/templates/index.html +++ b/app/web/templates/index.html @@ -64,7 +64,7 @@
{{ update.status }}
Posted {{ update.timestamp.strftime('%Y-%m-%d %H:%M') }} UTC
- {{ update.text }} + {{ update.text | safe }}

From 75a1705e905cd991a01f8cf243d57e8b82310504 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Tue, 14 May 2024 10:30:53 +0200 Subject: [PATCH 11/14] func handling_statuses added --- app/api/routes.py | 97 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index 00fab38..9802010 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -85,6 +85,45 @@ def create_new_incident(target_component, impact, text): return new_incident +def handling_statuses( + incident, + comp_with_attrs=None, + dst_incident=None, + new_incident=None, + action=None, + impact=None, + impacts=None +): + if action: + url_s = url_for('web.incident', incident_id=incident.id) + link_s = f"{incident.text}" + + if action == "move": + if dst_incident: + url_d = url_for('web.incident', incident_id=dst_incident.id) + link_d = f"{dst_incident.text}" + update_s = ( + f"{comp_with_attrs} moved to {link_d}" + ) + update_d = f"{comp_with_attrs} moved from {link_s}" + elif new_incident: + url_d = url_for('web.incident', incident_id=new_incident.id) + link_d = f"{new_incident.text}" + update_s = f"{comp_with_attrs} moved to {link_d}" + update_n = f"{comp_with_attrs} moved from {link_s}" + elif action == "change_impact": + update_s = ( + f"impact changed from {impacts[incident.impact].key} to " + f"{impacts[impact].key}" + ) + if dst_incident: + update_incident_status(dst_incident, update_d) + if new_incident: + update_incident_status(new_incident, update_n) + + update_incident_status(incident, update_s) + + def handling_incidents( target_component, impact, @@ -100,22 +139,20 @@ def handling_incidents( f"moved to: '{dst_incident.text}'" ) current_app.logger.debug(f"{src_incident.text} CLOSED") - - url_d = url_for('web.incident', incident_id=dst_incident.id) - url_s = url_for('web.incident', incident_id=src_incident.id) - link_s = f"{dst_incident.text}" - link_d = f"{src_incident.text}" - update_s = f"{comp_with_attrs} moved to {link_s}, closed by system" - update_d = f"{comp_with_attrs} moved from {link_d}" - - update_incident_status(src_incident, update_s) - update_incident_status(dst_incident, update_d) - src_incident.end_date = datetime.utcnow() dst_incident.components.append(target_component) + handling_statuses( + src_incident, + comp_with_attrs, + dst_incident=dst_incident, + action="move" + ) + db.session.commit() + update_incident_status(src_incident, "CLOSED BY SYSTEM") db.session.commit() return dst_incident elif len(src_incident.components) == 1 and not dst_incident: + # logging current_app.logger.debug( f"Component: {target_component} is present in the incident: " f"'{src_incident.text}'" @@ -125,12 +162,11 @@ def handling_incidents( f"changing the impact from: {impacts[src_incident.impact].key}" f"to {impacts[impact].key}" ) - update_incident_status( + handling_statuses( src_incident, - ( - f"impact changed from {impacts[src_incident.impact].key} " - f"to {impacts[impact].key}" - ) + action="change_impact", + impact=impact, + impacts=impacts ) src_incident.impact = impact db.session.commit() @@ -140,19 +176,14 @@ def handling_incidents( f"{target_component} moved from {src_incident.text} to " f"{dst_incident.text}" ) - - url_d = url_for('web.incident', incident_id=dst_incident.id) - url_s = url_for('web.incident', incident_id=src_incident.id) - link_s = f"{dst_incident.text}" - link_d = f"{src_incident.text}" - update_s = f"{comp_with_attrs} moved to {link_s}" - update_d = f"{comp_with_attrs} moved from {link_d}" - - update_incident_status(src_incident, update_s) - update_incident_status(dst_incident, update_d) - src_incident.components.remove(target_component) dst_incident.components.append(target_component) + handling_statuses( + src_incident, + comp_with_attrs, + dst_incident=dst_incident, + action="move" + ) db.session.commit() return dst_incident elif len(src_incident.components) > 1 and not dst_incident: @@ -162,12 +193,16 @@ def handling_incidents( current_app.logger.debug( f"{target_component} moved from {src_incident.text} to new one" ) - update_incident_status( + src_incident.components.remove(target_component) + new_incident = create_new_incident(target_component, impact, text) + handling_statuses( src_incident, - f"{comp_with_attrs} moved to new incident" + comp_with_attrs, + new_incident=new_incident, + action="move", ) - src_incident.components.remove(target_component) - return create_new_incident(target_component, impact, text) + db.session.commit() + return new_incident else: current_app.logger.error("Unexpected ERROR") From a66bb1c892649ab93932d386acc25a287987e44f Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Fri, 17 May 2024 19:55:27 +0200 Subject: [PATCH 12/14] web form "updates" fixed --- app/api/routes.py | 1 - app/web/routes.py | 52 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index 9802010..f25383c 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -152,7 +152,6 @@ def handling_incidents( db.session.commit() return dst_incident elif len(src_incident.components) == 1 and not dst_incident: - # logging current_app.logger.debug( f"Component: {target_component} is present in the incident: " f"'{src_incident.text}'" diff --git a/app/web/routes.py b/app/web/routes.py index 632f124..8b494dd 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -126,12 +126,29 @@ def new_incident(current_user): messages_to = [] for comp in incident_components: if comp in inc.components: - messages_to.append( - f"{comp} moved to {new_incident}" + comp_name = comp.name + comp_attributes = comp.attributes + comp_attributes_str = ", ".join( + [ + f"{attr.value}" for attr in comp_attributes + ] ) - messages_from.append( - f"{comp} moved from {inc}" + comp_with_attrs = f"{comp_name} ({comp_attributes_str})" + url_s = url_for( + 'web.incident', + incident_id=inc.id ) + link_s = f"{inc.text}" + url_d = url_for( + 'web.incident', + incident_id=new_incident.id + ) + link_d = f"{new_incident.text}" + update_s = f"{comp_with_attrs} moved to {link_d}" + update_n = f"{comp_with_attrs} moved from {link_s}" + messages_to.append(update_s) + messages_from.append(update_n) + if len(inc.components) > 1: inc.components.remove(comp) else: @@ -220,13 +237,36 @@ def separate_incident(current_user, incident_id, component_id): f"{new_incident} opened by {get_user_string(current_user)}" ) + comp_name = component.name + comp_attributes = component.attributes + comp_attributes_str = ", ".join( + [ + f"{attr.value}" for attr in comp_attributes + ] + ) + comp_with_attrs = f"{comp_name} ({comp_attributes_str})" + + url_s = url_for( + 'web.incident', + incident_id=incident.id + ) + link_s = f"{incident.text}" + url_d = url_for( + 'web.incident', + incident_id=new_incident.id + ) + link_d = f"{new_incident.text}" + + update_s = f"{comp_with_attrs} moved to {link_d}" + update_n = f"{comp_with_attrs} moved from {link_s}" + update_incident( incident, - f"{component} moved to {new_incident}" + update_s ) update_incident( new_incident, - f"{component} moved from {incident}" + update_n ) db.session.commit() return redirect("/") From d137b2debf05c7f8e362462ad2aec9ccbda5627b Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Wed, 22 May 2024 12:41:53 +0200 Subject: [PATCH 13/14] Existing incident response changed --- app/api/routes.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index f25383c..ae9b01c 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -372,7 +372,19 @@ def post(self, data): "requested impact equal or less - not modifying " "or opening new incident" ) - return incident + existing_incident = incident + response = { + "message": ( + "Incident with this the component " + "already exists." + ), + "targetComponent": comp_with_attrs, + "existingIncidentId": existing_incident.id, + "existingIncidentTitle": existing_incident.text, + "details": "Check your request parameters" + } + return jsonify(response), 409 + incidents = Incident.get_active() if not incidents: current_app.logger.debug("No active incidents - opening new one") @@ -420,7 +432,18 @@ def post(self, data): f"Active incident for {target_component} - " "not modifying or opening new incident" ) - return incident + existing_incident = incident + response = { + "message": ( + "Incident with this the component " + "already exists." + ), + "targetComponent": comp_with_attrs, + "existingIncidentId": existing_incident.id, + "existingIncidentTitle": existing_incident.text, + "details": "Check your request parameters" + } + return jsonify(response), 409 @bp.route("/v1/incidents", methods=["GET"]) From f1b17b6de35f399b5d8cc0cc59b4c124d96bbe15 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Wed, 22 May 2024 12:44:03 +0200 Subject: [PATCH 14/14] the extra point in the message text has been removed (typo) --- app/api/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/routes.py b/app/api/routes.py index ae9b01c..caea313 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -376,7 +376,7 @@ def post(self, data): response = { "message": ( "Incident with this the component " - "already exists." + "already exists" ), "targetComponent": comp_with_attrs, "existingIncidentId": existing_incident.id, @@ -436,7 +436,7 @@ def post(self, data): response = { "message": ( "Incident with this the component " - "already exists." + "already exists" ), "targetComponent": comp_with_attrs, "existingIncidentId": existing_incident.id,