From 0def901d4defe49ec0c70bba35c19fc6db3f307f Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Thu, 19 Dec 2024 21:50:13 -0300 Subject: [PATCH 1/5] Add missing exclude parameters to test_find_messages.py. --- tests/test_find_messages.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_find_messages.py b/tests/test_find_messages.py index 11e9f38..ae67951 100644 --- a/tests/test_find_messages.py +++ b/tests/test_find_messages.py @@ -267,7 +267,15 @@ def test_collection( ) # exclude_key for collection arguments for arrays - for field in ("tags",): + for field in ( + "tags", + "systems", + "subsystems", + "cscs", + "components", + "primary_software_components", + "primary_hardware_components", + ): # Scramble the messages and use two field list values # from the first message with at least two values messages_to_search = random.sample(messages, len(messages)) @@ -451,7 +459,6 @@ def is_valid_predicate(message: MessageDictT) -> bool: fields = list(MESSAGE_FIELDS) str_fields = set( ( - "instrument", "message_text", "level", "user_id", From a5c2333134b5dab7c91364ff6a99910219b1df7e Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Mon, 16 Dec 2024 18:28:37 -0300 Subject: [PATCH 2/5] Add new components_json field to the jira_fields table. Also add deprecation advice and code comments for systems, subsystems, cscs, components, primary_software_components and primary_hardware_components. --- ...3f83_add_components_json_field_to_jira_.py | 42 ++++ src/narrativelog/create_tables.py | 16 +- src/narrativelog/message.py | 45 +++- src/narrativelog/routers/add_message.py | 59 +++++- src/narrativelog/routers/edit_message.py | 57 ++++- src/narrativelog/routers/find_messages.py | 194 ++++++++++++++++-- 6 files changed, 382 insertions(+), 31 deletions(-) create mode 100644 alembic/versions/49ef39173f83_add_components_json_field_to_jira_.py diff --git a/alembic/versions/49ef39173f83_add_components_json_field_to_jira_.py b/alembic/versions/49ef39173f83_add_components_json_field_to_jira_.py new file mode 100644 index 0000000..c2f599d --- /dev/null +++ b/alembic/versions/49ef39173f83_add_components_json_field_to_jira_.py @@ -0,0 +1,42 @@ +"""add components_json field to jira_fields table + +Revision ID: 49ef39173f83 +Revises: 54f755dbdb6f +Create Date: 2024-12-18 17:01:39.676895 + +""" +import logging + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "49ef39173f83" +down_revision = "54f755dbdb6f" +branch_labels = None +depends_on = None + + +JIRA_FIELDS_TABLE_NAME = "jira_fields" + + +def upgrade(log: logging.Logger, table_names: set[str]) -> None: + if JIRA_FIELDS_TABLE_NAME not in table_names: + log.info(f"No {JIRA_FIELDS_TABLE_NAME} table; nothing to do") + return + log.info("Add 'components_json'") + + op.add_column( + JIRA_FIELDS_TABLE_NAME, + sa.Column("components_json", sa.JSON(), nullable=True), + ) + + +def downgrade(log: logging.Logger, table_names: set[str]) -> None: + if JIRA_FIELDS_TABLE_NAME not in table_names: + log.info(f"No {JIRA_FIELDS_TABLE_NAME} table; nothing to do") + return + + log.info("Drop 'components_json'") + op.drop_column(JIRA_FIELDS_TABLE_NAME, "components_json") diff --git a/src/narrativelog/create_tables.py b/src/narrativelog/create_tables.py index 9ed05c0..5b95d57 100644 --- a/src/narrativelog/create_tables.py +++ b/src/narrativelog/create_tables.py @@ -8,7 +8,7 @@ import sqlalchemy as sa import sqlalchemy.types as saty -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import JSONB, UUID # Length of the site_id field. SITE_ID_LEN = 16 @@ -60,8 +60,14 @@ def create_message_table(metadata: sa.MetaData) -> sa.Table: sa.Column("date_invalidated", saty.DateTime(), nullable=True), sa.Column("parent_id", UUID(as_uuid=True), nullable=True), # Added 2022-07-19 + # 'systems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead sa.Column("systems", saty.ARRAY(sa.Text), nullable=True), + # 'subsystems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead sa.Column("subsystems", saty.ARRAY(sa.Text), nullable=True), + # 'cscs' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead sa.Column("cscs", saty.ARRAY(sa.Text), nullable=True), # Added 2022-07-37 sa.Column("date_end", saty.DateTime(), nullable=True), @@ -110,10 +116,18 @@ def create_jira_fields_table(metadata: sa.MetaData) -> sa.Table: sa.Column( "id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ), + # Added 2024-12-16 + sa.Column("components_json", JSONB, nullable=True), + # 'components' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead sa.Column("components", saty.ARRAY(sa.Text), nullable=True), + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead sa.Column( "primary_software_components", saty.ARRAY(sa.Text), nullable=True ), + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead sa.Column( "primary_hardware_components", saty.ARRAY(sa.Text), nullable=True ), diff --git a/src/narrativelog/message.py b/src/narrativelog/message.py index d009a4e..0de4059 100644 --- a/src/narrativelog/message.py +++ b/src/narrativelog/message.py @@ -46,13 +46,21 @@ class Message(BaseModel): ) # Added 2022-07-19 systems: None | list[str] = Field( - title="Zero or more system names.", + title="Zero or more system names. " + "This field is deprecated and will be removed in v1.0.0. " + "Please use 'components_json' instead.", + ) + subsystems: None | list[str] = Field( + title="Zero or more subsystem names. " + "This field is deprecated and will be removed in v1.0.0. " + "Please use 'components_json' instead.", ) - subsystems: None | list[str] = Field(title="Zero or more subsystem names.") cscs: None | list[str] = Field( title="Zero or more CSCs names. " "Each entry should be in the form 'name' or 'name:index', " - "where 'name' is the SAL component name and 'index' is the SAL index." + "where 'name' is the SAL component name and 'index' is the SAL index. " + "This field is deprecated and will be removed in v1.0.0. " + "Please use 'components_json' instead.", ) # Added 2022-07-27 date_end: None | datetime.datetime = Field( @@ -61,15 +69,21 @@ class Message(BaseModel): # Added 2023-08-10 components: None | list[str] = Field( title="Zero or more component names. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "This field is deprecated and will be removed in v1.0.0. " + "Please use 'components_json' instead.", ) primary_software_components: None | list[str] = Field( title="Zero or more primary software component names. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "This field is deprecated and will be removed in v1.0.0. " + "Please use 'components_json' instead.", ) primary_hardware_components: None | list[str] = Field( title="Zero or more primary hardware component names. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "This field is deprecated and will be removed in v1.0.0. " + "Please use 'components_json' instead.", ) # Added 2023-10-24 category: None | str = Field( @@ -78,6 +92,18 @@ class Message(BaseModel): time_lost_type: None | str = Field( title="Type of time lost.", ) + # Added 2024-12-16 + components_json: None | dict = Field( + default_factory=dict, + title="JSON representation of systems, subsystems and components " + "on the OBS jira project. An example of a valid payload is: " + '`{"systems": ["Simonyi", "AuxTel"], ' + '{"subsystems": ["TMA", "Mount"], ' + '{"components": ["MTMount CSC"]}`. ' + "For a full list of valid systems, subsystems and components " + "please refer to: https://rubinobs.atlassian.net/wiki/spaces/LSSTCOM" + "/pages/53741849/Systems+Sub-Systems+and+Components+Proposal+for+JIRA", + ) class Config: orm_mode = True @@ -85,9 +111,16 @@ class Config: JIRA_FIELDS = ( + # 'components' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead "components", + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "primary_software_components", + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "primary_hardware_components", + "components_json", ) MESSAGE_FIELDS = tuple( set(Message.schema()["properties"].keys()) - set(JIRA_FIELDS) diff --git a/src/narrativelog/routers/add_message.py b/src/narrativelog/routers/add_message.py index 74c41c7..d0a6f5d 100644 --- a/src/narrativelog/routers/add_message.py +++ b/src/narrativelog/routers/add_message.py @@ -41,37 +41,62 @@ async def add_message( systems: None | list[str] = fastapi.Body( default=None, - description="Zero or more systems to which the message applies.", + description="Zero or more systems to which the message applies. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), subsystems: None | list[str] = fastapi.Body( default=None, - description="Zero or more subsystems to which the message applies", + description="Zero or more subsystems to which the message applies. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), cscs: None | list[str] = fastapi.Body( default=None, description="Zero or more CSCs to which the message applies. " "Each entry should be in the form 'name' or 'name:index', " - "where 'name' is the SAL component name and 'index' is the SAL index.", + "where 'name' is the SAL component name and 'index' is the SAL index. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), components: None | list[str] = fastapi.Body( default=None, description="Zero or more components to which the message applies. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), primary_software_components: None | list[str] = fastapi.Body( default=None, description="Primary software components to which the message applies. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), primary_hardware_components: None | list[str] = fastapi.Body( default=None, description="Primary hardware components to which the message applies. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", + ), + components_json: None + | dict = fastapi.Body( + default=None, + description="JSON representation of systems, subsystems and components " + "on the OBS jira project. An example of a valid payload is: " + '`{"systems": ["Simonyi", "AuxTel"], ' + '{"subsystems": ["TMA", "Mount"], ' + '{"components": ["MTMount CSC"]}`. ' + "For a full list of valid systems, subsystems and components " + "please refer to: [Systems, Sub-Systems and Components Proposal " + "for the OBS Jira project](https://rubinobs.atlassian.net/wiki/spaces/LSSTCOM/" + "pages/53741849/Systems+Sub-Systems+and+Components+Proposal+for+JIRA)", ), urls: list[str] = fastapi.Body( default=[], @@ -147,8 +172,14 @@ async def add_message( user_agent=user_agent, is_human=is_human, date_added=curr_tai.tai.datetime, + # 'systems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead systems=systems, + # 'subsystems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead subsystems=subsystems, + # 'cscs' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead cscs=cscs, category=category, time_lost_type=time_lost_type, @@ -166,17 +197,33 @@ async def add_message( if any( field is not None for field in ( + # 'components' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead components, + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead primary_software_components, + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead primary_hardware_components, + components_json, ) ): result_jira_fields = await connection.execute( jira_fields_table.insert() .values( + # 'components' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead components=components, + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. + # Please use 'components_json' instead primary_software_components=primary_software_components, + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. + # Please use 'components_json' instead primary_hardware_components=primary_hardware_components, + components_json=components_json, message_id=row_message.id, ) .returning(sa.literal_column("*")) diff --git a/src/narrativelog/routers/edit_message.py b/src/narrativelog/routers/edit_message.py index a1efe7c..c9a1e7e 100644 --- a/src/narrativelog/routers/edit_message.py +++ b/src/narrativelog/routers/edit_message.py @@ -44,13 +44,17 @@ async def edit_message( | list[str] = fastapi.Body( default=None, description="Zero or more systems to which the message applied. " - "If specified, replaces all existing entries.", + "If specified, replaces all existing entries." + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), subsystems: None | list[str] = fastapi.Body( default=None, description="Zero or more subsystems to which the message applies. " - "If specified, replaces all existing entries.", + "If specified, replaces all existing entries." + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), cscs: None | list[str] = fastapi.Body( @@ -58,28 +62,49 @@ async def edit_message( description="Zero or more CSCs to which the message applies. " "Each entry should be in the form 'name' or 'name:index', " "where 'name' is the SAL component name and 'index' is the SAL index. " - "If specified, replaces all existing entries.", + "If specified, replaces all existing entries." + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), components: None | list[str] = fastapi.Body( default=None, description="Zero or more components to which the message applies. " "Each entry should be a valid component name entry on the OBS jira project. " - "If specified, replaces all existing entries.", + "If specified, replaces all existing entries." + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), primary_software_components: None | list[str] = fastapi.Body( default=None, description="Primary software components to which the message applies. " "Each entry should be a valid component name entry on the OBS jira project. " - "If specified, replaces all existing entries.", + "If specified, replaces all existing entries." + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", ), primary_hardware_components: None | list[str] = fastapi.Body( default=None, description="Primary hardware components to which the message applies. " "Each entry should be a valid component name entry on the OBS jira project. " - "If specified, replaces all existing entries.", + "If specified, replaces all existing entries." + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_json' instead.", + ), + components_json: None + | dict = fastapi.Body( + default=None, + description="JSON representation of systems, subsystems and components " + "on the OBS jira project. An example of a valid payload is: " + '`{"systems": ["Simonyi", "AuxTel"], ' + '{"subsystems": ["TMA", "Mount"], ' + '{"components": ["MTMount CSC"]}`. ' + "For a full list of valid systems, subsystems and components " + "please refer to: [Systems, Sub-Systems and Components Proposal " + "for the OBS Jira project](https://rubinobs.atlassian.net/wiki/spaces/LSSTCOM/" + "pages/53741849/Systems+Sub-Systems+and+Components+Proposal+for+JIRA)", ), urls: None | list[str] = fastapi.Body( @@ -143,12 +168,25 @@ async def edit_message( "message_text", "level", "tags", + # 'systems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead "systems", + # 'subsystems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead "subsystems", + # 'cscs' field is deprecated and will be removed in v1.0.0. + # Please use 'components_json' instead "cscs", + # 'components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "components", + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "primary_software_components", + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "primary_hardware_components", + "components_json", "category", "time_lost_type", "urls", @@ -165,9 +203,16 @@ async def edit_message( request_data[name] = value jira_update_params = { + # 'components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "components", + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "primary_software_components", + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_json' instead "primary_hardware_components", + "components_json", } async with state.narrativelog_db.engine.begin() as connection: diff --git a/src/narrativelog/routers/find_messages.py b/src/narrativelog/routers/find_messages.py index e345e88..7b0b104 100644 --- a/src/narrativelog/routers/find_messages.py +++ b/src/narrativelog/routers/find_messages.py @@ -3,6 +3,7 @@ import datetime import enum import http +import json import fastapi import sqlalchemy as sa @@ -101,42 +102,54 @@ async def find_messages( default=None, description="System names or fragments of names. All messages " "with a system that matches any of these are included. " - "Repeat the parameter for each value.", + "Repeat the parameter for each value. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), exclude_systems: None | list[str] = fastapi.Query( default=None, description="System names or fragments of names. All messages " "with a system that matches any of these are excluded. " - "Repeat the parameter for each value.", + "Repeat the parameter for each value. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), subsystems: None | list[str] = fastapi.Query( default=None, description="Subsystem names or fragments of names. All messages " "with a subsystem that matches any of these are included. " - "Repeat the parameter for each value.", + "Repeat the parameter for each value. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), exclude_subsystems: None | list[str] = fastapi.Query( default=None, description="Subsystem names or fragments of names. All messages " "with a subsystem that matches any of these are excluded. " - "Repeat the parameter for each value.", + "Repeat the parameter for each value. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), cscs: None | list[str] = fastapi.Query( default=None, description="CSC names or fragments of CSC names, " "of which at least one must be present. " - "Repeat the parameter for each value.", + "Repeat the parameter for each value. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), exclude_cscs: None | list[str] = fastapi.Query( default=None, description="CSC names or fragments of CSC names, " "of which all must be absent. " - "Repeat the parameter for each value.", + "Repeat the parameter for each value. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), components: None | list[str] = fastapi.Query( @@ -144,7 +157,9 @@ async def find_messages( description="Component names or fragments of names. All messages " "with a component that matches any of these are included. " "Repeat the parameter for each value. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), exclude_components: None | list[str] = fastapi.Query( @@ -152,7 +167,9 @@ async def find_messages( description="Component names or fragments of names. All messages " "with a component that matches any of these are excluded. " "Repeat the parameter for each value. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), primary_software_components: None | list[str] = fastapi.Query( @@ -160,7 +177,9 @@ async def find_messages( description="Primary software components names or fragments of names. " "All messages with a component that matches any of these are included. " "Repeat the parameter for each value. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), exclude_primary_software_components: None | list[str] = fastapi.Query( @@ -168,7 +187,9 @@ async def find_messages( description="Primary software components names or fragments of names. " "All messages with a component that matches any of these are excluded. " "Repeat the parameter for each value. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), primary_hardware_components: None | list[str] = fastapi.Query( @@ -176,14 +197,73 @@ async def find_messages( description="Primary hardware components names or fragments of names. " "All messages with a component that matches any of these are included. " "Repeat the parameter for each value. " - "Each entry should be a valid component name entry on the OBS jira project.", + "Each entry should be a valid component name entry on the OBS jira project. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", ), exclude_primary_hardware_components: None | list[str] = fastapi.Query( default=None, description="Primary hardware components names or fragments of names. " "All messages with a component that matches any of these are excluded. " - "Repeat the parameter for each value.", + "Repeat the parameter for each value. " + "**This field is deprecated and will be removed in v1.0.0**. " + "Please use 'components_path' instead.", + ), + components_path: None + | str = fastapi.Query( + default=None, + description="Components structure in JSON format to include. " + 'All messages with a "components_json" field that ' + "matches at least a key with any of the values specified within " + 'the "components_path" are included. The JSON object represents the current ' + "hierarchy of systems, subsystems and components on the OBS Jira project: " + '`{"systems": ["system1", ..., "systemN"], ' + '"subsystems": ["subsystem1", ..., "subsystemN"], ' + '"components": ["component1", ..., "componentN"]}`. ' + 'E.g. Setting "components_path" to `{"systems": ["AuxTel", "Simonyi"], ' + '"subsystems": ["Mount", "TMA"], ' + '"components": ["ATMCS CSC"]}` will match ' + 'all messages that have "AuxTel" OR ' + '"Simonyi" values under the "systems" key ' + 'OR have "Mount" OR "TMA" values under the "subsystems" key ' + 'OR have "ATMCS CSC" value under the "components" key. ' + 'Note that setting "components_path" to `{"subsystems": ' + '["Mount", "TMA"]}` is the same as setting it to ' + '`{"subsystems": ["TMA", "Mount"]}` so will end up ' + 'in the same result. Also setting it to `{"subsystems": []}` will ' + 'include all messages that have at least the "subsystems" key defined. ' + "Any key with a value that is not a list will be ignored. " + 'Furthermore setting "components_path" to `{}` will have no effect and ' + "an invalid JSON will raise a 400 error.", + ), + exclude_components_path: None + | str = fastapi.Query( + default=None, + description="Components structure in JSON format to exclude. " + 'All messages with a "components_json" field that ' + "matches at least a key with any of the values specified within " + 'the "exclude_components_path" are excluded. The JSON object ' + "represents the current hierarchy of systems, subsystems " + "and components on the OBS Jira project: " + '`{"systems": ["system1", ..., "systemN"], ' + '"subsystems": ["subsystem1", ..., "subsystemN"], ' + '"components": ["component1", ..., "componentN"]}`. ' + 'E.g. Setting "exclude_components_path" to `{"systems": ["AuxTel", ' + '"Simonyi"], "subsystems": ["Mount", "TMA"], ' + '"components": ["ATMCS CSC"]}` will match ' + 'all messages that have "AuxTel" OR ' + '"Simonyi" values under the "systems" key ' + 'OR have "Mount" OR "TMA" values under the "subsystems" key ' + 'OR have "ATMCS CSC" value under the "components" key. ' + 'Note that setting "exclude_components_path" to `{"subsystems": ' + '["Mount", "TMA"]}` is the same as setting it to ' + '`{"subsystems": ["TMA", "Mount"]}` so will end up ' + 'in the same result. Also setting it to `{"subsystems": []}` will ' + 'include all messages that have at least the "subsystems" key defined. ' + "Any key with a value that is not a list will be ignored. " + 'Furthermore setting "exclude_components_path" to `{}` will have no effect ' + "and an invalid JSON will raise a 400 error.", ), urls: None | list[str] = fastapi.Query( @@ -299,18 +379,44 @@ async def find_messages( "max_level", "user_ids", "user_agents", + # 'systems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "systems", + # 'exclude_systems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "exclude_systems", + # 'subsystems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "subsystems", + # 'exclude_subsystems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "exclude_subsystems", + # 'cscs' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "cscs", + # 'exclude_cscs' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "exclude_cscs", + # 'components' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "components", + # 'exclude_components' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "exclude_components", + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "primary_software_components", + # 'exclude_primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_primary_software_components", + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "primary_hardware_components", + # 'exclude_primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_primary_hardware_components", + "components_path", + "exclude_components_path", "tags", "exclude_tags", "urls", @@ -386,8 +492,14 @@ async def find_messages( conditions.append(column == None) # noqa elif key in { "tags", + # 'systems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "systems", + # 'subsystems' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "subsystems", + # 'cscs' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "cscs", "urls", }: @@ -406,16 +518,28 @@ async def find_messages( column = message_table.columns[key] conditions.append(column.op("&&")(value)) elif key in { + # 'components' field is deprecated and will be removed in v1.0.0. + # Please use 'components_path' instead "components", + # 'primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "primary_software_components", + # 'primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "primary_hardware_components", }: column = jira_fields_table.columns[key] conditions.append(column.op("&&")(value)) elif key in { "exclude_tags", + # 'exclude_systems' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_systems", + # 'exclude_subsystems' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_subsystems", + # 'exclude_cscs' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_cscs", }: # Value is a list; field name is the end of the key. @@ -425,13 +549,59 @@ async def find_messages( column = message_table.columns[column_name] conditions.append(sa.sql.not_(column.op("&&")(value))) elif key in { + # 'exclude_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_components", + # 'exclude_primary_software_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_primary_software_components", + # 'exclude_primary_hardware_components' field is deprecated + # and will be removed in v1.0.0. Please use 'components_path' instead "exclude_primary_hardware_components", }: column_name = key[8:] column = jira_fields_table.columns[column_name] conditions.append(sa.sql.not_(column.op("&&")(value))) + elif key in {"components_path"}: + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as error: + raise fastapi.HTTPException( + status_code=http.HTTPStatus.BAD_REQUEST, + detail=f"Invalid JSON in {key}: {error}", + ) + column_name = "components_json" + column = jira_fields_table.columns[column_name] + individual_conditions = [] + for key in parsed_value: + value = parsed_value[key] + if not value or not isinstance(value, list): + continue + for element in value: + path = {key: [element]} + individual_conditions.append(column.contains(path)) + conditions.append(sa.sql.or_(*individual_conditions)) + elif key in {"exclude_components_path"}: + try: + parsed_value = json.loads(value) + except json.JSONDecodeError as error: + raise fastapi.HTTPException( + status_code=http.HTTPStatus.BAD_REQUEST, + detail=f"Invalid JSON in {key}: {error}", + ) + column_name = "components_json" + column = jira_fields_table.columns[column_name] + individual_conditions = [] + for key in parsed_value: + value = parsed_value[key] + if not value or not isinstance(value, list): + continue + for element in value: + path = {key: [element]} + individual_conditions.append(column.contains(path)) + conditions.append( + sa.sql.not_(sa.sql.or_(*individual_conditions)) + ) elif key in { "site_ids", "instruments", From 8823c65c06ff8fd1b24457f66abeb75e9ce6d50b Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Sanchez Date: Wed, 18 Dec 2024 17:40:44 -0300 Subject: [PATCH 3/5] Extend tests to support new components_json field. --- src/narrativelog/testutils.py | 14 ++++++++++- tests/test_add_message.py | 7 ++++++ tests/test_edit_message.py | 5 ++++ tests/test_find_messages.py | 45 +++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/narrativelog/testutils.py b/src/narrativelog/testutils.py index 4673cba..0ccf10d 100644 --- a/src/narrativelog/testutils.py +++ b/src/narrativelog/testutils.py @@ -366,6 +366,12 @@ def random_message() -> MessageDictT: elif random_value > 0.25: date_begin = random_date() + test_components_json = { + "systems": random_strings(TEST_SYSTEMS), + "subsystems": random_strings(TEST_SUBSYSTEMS), + "components": random_strings(TEST_COMPONENTS), + } + message = dict( id=None, site_id=TEST_SITE_ID, @@ -396,6 +402,8 @@ def random_message() -> MessageDictT: primary_hardware_components=random_strings( TEST_PRIMARY_HARDWARE_COMPONENTS ), + # Added 2024-12-16 + components_json=test_components_json, # Added 2023-10-24 category=random_str(nchar=CATEGORY_LEN), time_lost_type=random.choice(["fault", "weather"]), @@ -502,11 +510,14 @@ async def create_test_database( pruned_message = message.copy() del pruned_message["is_valid"] # Do not insert "components", - # "primary_software_components", or "primary_hardware_components" + # "primary_software_components", + # "primary_hardware_components", + # or "components_json" # because they are in a separate table. del pruned_message["components"] del pruned_message["primary_software_components"] del pruned_message["primary_hardware_components"] + del pruned_message["components_json"] # Insert the message result_message = await connection.execute( @@ -529,6 +540,7 @@ async def create_test_database( primary_hardware_components=message[ "primary_hardware_components" ], + components_json=message["components_json"], message_id=data_message.id, ) .returning(literal_column("*")) diff --git a/tests/test_add_message.py b/tests/test_add_message.py index e2b051d..fc46f26 100644 --- a/tests/test_add_message.py +++ b/tests/test_add_message.py @@ -90,6 +90,13 @@ async def test_add_message(self) -> None: add_args_full["primary_hardware_components"] = random_strings( TEST_PRIMARY_HARDWARE_COMPONENTS ) + + test_components_json = { + "systems": random_strings(TEST_SYSTEMS), + "subsystems": random_strings(TEST_SUBSYSTEMS), + "components": random_strings(TEST_COMPONENTS), + } + add_args_full["components_json"] = test_components_json add_args_full["category"] = "test" add_args_full["time_lost_type"] = random.choice( ["fault", "weather"] diff --git a/tests/test_edit_message.py b/tests/test_edit_message.py index 1b0ea0c..3679a6d 100644 --- a/tests/test_edit_message.py +++ b/tests/test_edit_message.py @@ -85,6 +85,11 @@ async def test_edit_message(self) -> None: "new primary_hardware_component 1", "new primary_hardware_component 2", ], + components_json={ + "systems": ["new system 1", "new system 2"], + "subsystems": ["new subsystem 1", "new subsystem 2"], + "components": ["new component 1", "new component 2"], + }, category="New category", time_lost_type=random.choice(["fault", "weather"]), date_begin="2024-01-01T00:00:00", diff --git a/tests/test_find_messages.py b/tests/test_find_messages.py index ae67951..3afe6da 100644 --- a/tests/test_find_messages.py +++ b/tests/test_find_messages.py @@ -1,6 +1,7 @@ import collections.abc import http import itertools +import json import random import typing import unittest @@ -341,6 +342,50 @@ def test_contains( find_args_predicates.append(({field: value}, test_contains)) + # "Contains" arguments for JSON fields: these specify a + # JSON path to match. + for field in ("components_json",): + # Scramble the messages and use the first + # message with at least a key with two values + messages_to_search = random.sample(messages, len(messages)) + for message in messages_to_search: + components_json = message[field] + for key in components_json: + if len(components_json[key]) >= 2: + first_two_values = components_json[key][0:2] + values = [(key, val) for val in first_two_values] + path = json.dumps({key: first_two_values}) + break + + @doc_str(f"{path!r} in message[{field!r}]") + def test_contains_path( + message: MessageDictT, + field: str = field, + values: list[tuple[str, str]] = values, + ) -> bool: + matches = [ + val in message[field][key] for key, val in values + ] + return any(matches) + + def test_contains_exclude_path( + message: MessageDictT, + field: str = field, + values: list[tuple[str, str]] = values, + ) -> bool: + matches = [ + val in message[field][key] for key, val in values + ] + return not any(matches) + + find_args_predicates += [ + ({"components_path": path}, test_contains_path), + ( + {"exclude_components_path": path}, + test_contains_exclude_path, + ), + ] + # has_ arguments (for fields that may be null). for field in ( "date_begin", From 36ad76944602b0d07cdd5b058281990cae9429b6 Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Date: Thu, 19 Dec 2024 23:14:10 -0300 Subject: [PATCH 4/5] Add get_version endpoint to identify REST API current version --- src/narrativelog/main.py | 2 ++ src/narrativelog/routers/get_version.py | 27 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/narrativelog/routers/get_version.py diff --git a/src/narrativelog/main.py b/src/narrativelog/main.py index c5931fc..7b7a56e 100644 --- a/src/narrativelog/main.py +++ b/src/narrativelog/main.py @@ -10,6 +10,7 @@ find_messages, get_configuration, get_message, + get_version, ) app = fastapi.FastAPI() @@ -27,6 +28,7 @@ subapp.include_router(find_messages.router) subapp.include_router(get_configuration.router) subapp.include_router(get_message.router) +subapp.include_router(get_version.router) @subapp.get("/", response_class=fastapi.responses.HTMLResponse) diff --git a/src/narrativelog/routers/get_version.py b/src/narrativelog/routers/get_version.py new file mode 100644 index 0000000..c40c348 --- /dev/null +++ b/src/narrativelog/routers/get_version.py @@ -0,0 +1,27 @@ +__all__ = ["get_version"] + +import fastapi +import pydantic + +from .. import __version__ +from ..shared_state import SharedState, get_shared_state + +router = fastapi.APIRouter() + + +class Version(pydantic.BaseModel): + version: str = pydantic.Field(title="Current version of the REST API.") + + class Config: + orm_mode = True + from_attributes = True + + +@router.get("/version", response_model=Version) +@router.get("/version/", response_model=Version, include_in_schema=False) +async def get_version( + state: SharedState = fastapi.Depends(get_shared_state), +) -> Version: + """Get the current version of the package.""" + + return Version(version=__version__) From f31a7c93fa7a6704300471723a17c20e5851c10d Mon Sep 17 00:00:00 2001 From: Sebastian Aranda Sanchez Date: Mon, 6 Jan 2025 18:55:47 -0300 Subject: [PATCH 5/5] Refactor components_json field structure to represent hierarchy among systems, subsystems and components. --- src/narrativelog/message.py | 30 ++- src/narrativelog/routers/add_message.py | 30 ++- src/narrativelog/routers/find_messages.py | 243 +++++++++++++++------- src/narrativelog/utils.py | 7 + 4 files changed, 216 insertions(+), 94 deletions(-) create mode 100644 src/narrativelog/utils.py diff --git a/src/narrativelog/message.py b/src/narrativelog/message.py index 0de4059..1bc3c83 100644 --- a/src/narrativelog/message.py +++ b/src/narrativelog/message.py @@ -5,6 +5,8 @@ from pydantic import BaseModel, Field +from .utils import JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK + class Message(BaseModel): id: uuid.UUID = Field(title="Message ID: a UUID that is the primary key.") @@ -95,14 +97,26 @@ class Message(BaseModel): # Added 2024-12-16 components_json: None | dict = Field( default_factory=dict, - title="JSON representation of systems, subsystems and components " - "on the OBS jira project. An example of a valid payload is: " - '`{"systems": ["Simonyi", "AuxTel"], ' - '{"subsystems": ["TMA", "Mount"], ' - '{"components": ["MTMount CSC"]}`. ' - "For a full list of valid systems, subsystems and components " - "please refer to: https://rubinobs.atlassian.net/wiki/spaces/LSSTCOM" - "/pages/53741849/Systems+Sub-Systems+and+Components+Proposal+for+JIRA", + title=""" +JSON representation of systems, subsystems and components hierarchy + on the OBS jira project. An example of a valid payload is: + + { + "name": "AuxTel", + "children": [ + { + "name": "Dome", + "children": [ + { + "name": "AT Azimuth Drives" + } + ] + } + ] + } + +For a full list of valid systems, subsystems and components please refer to: """ + f"""{JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK}.""", ) class Config: diff --git a/src/narrativelog/routers/add_message.py b/src/narrativelog/routers/add_message.py index d0a6f5d..948a722 100644 --- a/src/narrativelog/routers/add_message.py +++ b/src/narrativelog/routers/add_message.py @@ -9,6 +9,7 @@ from ..message import Message from ..shared_state import SharedState, get_shared_state +from ..utils import JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK from .normalize_tags import TAG_DESCRIPTION, normalize_tags router = fastapi.APIRouter() @@ -88,15 +89,26 @@ async def add_message( components_json: None | dict = fastapi.Body( default=None, - description="JSON representation of systems, subsystems and components " - "on the OBS jira project. An example of a valid payload is: " - '`{"systems": ["Simonyi", "AuxTel"], ' - '{"subsystems": ["TMA", "Mount"], ' - '{"components": ["MTMount CSC"]}`. ' - "For a full list of valid systems, subsystems and components " - "please refer to: [Systems, Sub-Systems and Components Proposal " - "for the OBS Jira project](https://rubinobs.atlassian.net/wiki/spaces/LSSTCOM/" - "pages/53741849/Systems+Sub-Systems+and+Components+Proposal+for+JIRA)", + description=""" +JSON representation of systems, subsystems and components hierarchy + on the OBS jira project. An example of a valid payload is: + + { + "name": "AuxTel", + "children": [ + { + "name": "Dome", + "children": [ + { + "name": "AT Azimuth Drives" + } + ] + } + ] + } + +For a full list of valid systems, subsystems and components please refer to: """ + f"""{JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK}.""", ), urls: list[str] = fastapi.Body( default=[], diff --git a/src/narrativelog/routers/find_messages.py b/src/narrativelog/routers/find_messages.py index 7b0b104..64337bd 100644 --- a/src/narrativelog/routers/find_messages.py +++ b/src/narrativelog/routers/find_messages.py @@ -10,6 +10,7 @@ from ..message import MESSAGE_ORDER_BY_VALUES, Message from ..shared_state import SharedState, get_shared_state +from ..utils import JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK from .normalize_tags import TAG_DESCRIPTION, normalize_tags router = fastapi.APIRouter() @@ -211,59 +212,157 @@ async def find_messages( "Please use 'components_path' instead.", ), components_path: None - | str = fastapi.Query( + | list[str] = fastapi.Query( default=None, - description="Components structure in JSON format to include. " - 'All messages with a "components_json" field that ' - "matches at least a key with any of the values specified within " - 'the "components_path" are included. The JSON object represents the current ' - "hierarchy of systems, subsystems and components on the OBS Jira project: " - '`{"systems": ["system1", ..., "systemN"], ' - '"subsystems": ["subsystem1", ..., "subsystemN"], ' - '"components": ["component1", ..., "componentN"]}`. ' - 'E.g. Setting "components_path" to `{"systems": ["AuxTel", "Simonyi"], ' - '"subsystems": ["Mount", "TMA"], ' - '"components": ["ATMCS CSC"]}` will match ' - 'all messages that have "AuxTel" OR ' - '"Simonyi" values under the "systems" key ' - 'OR have "Mount" OR "TMA" values under the "subsystems" key ' - 'OR have "ATMCS CSC" value under the "components" key. ' - 'Note that setting "components_path" to `{"subsystems": ' - '["Mount", "TMA"]}` is the same as setting it to ' - '`{"subsystems": ["TMA", "Mount"]}` so will end up ' - 'in the same result. Also setting it to `{"subsystems": []}` will ' - 'include all messages that have at least the "subsystems" key defined. ' - "Any key with a value that is not a list will be ignored. " - 'Furthermore setting "components_path" to `{}` will have no effect and ' - "an invalid JSON will raise a 400 error.", + description=""" +List of strings with components structure in JSON format to include. + Matches all messages with at least a JSON path included in the "components_path" list. + Each element in the list is a JSON string as the object that represents the current + hierarchy of systems, subsystems and components on the OBS Jira project: + `System -> Subsystem -> Component` + (check """ + f"""{JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK}""" + """ for valid hierarchies). + E.g. Setting a "components_path" string (you can add multiple) to + + { + "name": "AuxTel", + "children": [ + { + "name": "Dome", + "children": [ + { + "name": "AT Azimuth Drives" + } + ] + } + ] + } + +will match all messages that have "AuxTel" as system AND + "Dome" as subsystem of "AuxTel" AND + "AT Azimuth Drives" as component of "Dome". + Note that setting "components_path" to + + { + "name": "AuxTel", + "children": [ + { + "name": "Dome" + }, + { + "name": "Mount" + } + ] + } + +Is the same as setting it to + + { + "name": "AuxTel", + "children": [ + { + "name": "Mount" + }, + { + "name": "Dome" + } + ] + } + +In other words, the order of the children is not important. + Also setting a `children` key as `[]` is the same as not defining it + (will be ignored). Furthermore setting a "components_path" string + to `{}` will have no effect and any invalid JSON will raise a 400 error. + Take in mind that values are case senstive. + +All defined JSON strings will be joined with an OR operator. + E.g. setting two "components_path" as + + { "name": "AuxTel" } + +and + + { "name": "Simonyi" } + +Will match all messages that have "AuxTel" OR "Simonyi" as system.""", ), exclude_components_path: None - | str = fastapi.Query( + | list[str] = fastapi.Query( default=None, - description="Components structure in JSON format to exclude. " - 'All messages with a "components_json" field that ' - "matches at least a key with any of the values specified within " - 'the "exclude_components_path" are excluded. The JSON object ' - "represents the current hierarchy of systems, subsystems " - "and components on the OBS Jira project: " - '`{"systems": ["system1", ..., "systemN"], ' - '"subsystems": ["subsystem1", ..., "subsystemN"], ' - '"components": ["component1", ..., "componentN"]}`. ' - 'E.g. Setting "exclude_components_path" to `{"systems": ["AuxTel", ' - '"Simonyi"], "subsystems": ["Mount", "TMA"], ' - '"components": ["ATMCS CSC"]}` will match ' - 'all messages that have "AuxTel" OR ' - '"Simonyi" values under the "systems" key ' - 'OR have "Mount" OR "TMA" values under the "subsystems" key ' - 'OR have "ATMCS CSC" value under the "components" key. ' - 'Note that setting "exclude_components_path" to `{"subsystems": ' - '["Mount", "TMA"]}` is the same as setting it to ' - '`{"subsystems": ["TMA", "Mount"]}` so will end up ' - 'in the same result. Also setting it to `{"subsystems": []}` will ' - 'include all messages that have at least the "subsystems" key defined. ' - "Any key with a value that is not a list will be ignored. " - 'Furthermore setting "exclude_components_path" to `{}` will have no effect ' - "and an invalid JSON will raise a 400 error.", + description=""" +List of strings with components structure in JSON format to exclude. + Exclude all messages with at least a JSON path + included in the "exclude_components_path" list. + Each element in the list is a JSON string as the object that represents the current + hierarchy of systems, subsystems and components on the OBS Jira project: + `System -> Subsystem -> Component` + (check """ + f"""{JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK}""" + """ for valid hierarchies). + E.g. Setting a "exclude_components_path" string (you can add multiple) to + + { + "name": "AuxTel", + "children": [ + { + "name": "Dome", + "children": [ + { + "name": "AT Azimuth Drives" + } + ] + } + ] + } + +will exclude all messages that have "AuxTel" as system AND + "Dome" as subsystem of "AuxTel" AND + "AT Azimuth Drives" as component of "Dome". + Note that setting "exclude_components_path" to + + { + "name": "AuxTel", + "children": [ + { + "name": "Dome" + }, + { + "name": "Mount" + } + ] + } + +Is the same as setting it to + + { + "name": "AuxTel", + "children": [ + { + "name": "Mount" + }, + { + "name": "Dome" + } + ] + } + +In other words, the order of the children is not important. + Also setting a `children` key as `[]` is the same as not defining it + (will be ignored). Furthermore setting a "exclude_components_path" string + to `{}` will have no effect and any invalid JSON will raise a 400 error. + Take in mind that values are case senstive. + +All defined JSON strings will be joined with an OR operator. + E.g. setting two "exclude_components_path" as + + { "name": "AuxTel" } + +and + + { "name": "Simonyi" } + +Will exclude all messages that have "AuxTel" OR "Simonyi" as system.""", ), urls: None | list[str] = fastapi.Query( @@ -563,42 +662,32 @@ async def find_messages( column = jira_fields_table.columns[column_name] conditions.append(sa.sql.not_(column.op("&&")(value))) elif key in {"components_path"}: - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as error: - raise fastapi.HTTPException( - status_code=http.HTTPStatus.BAD_REQUEST, - detail=f"Invalid JSON in {key}: {error}", - ) column_name = "components_json" column = jira_fields_table.columns[column_name] individual_conditions = [] - for key in parsed_value: - value = parsed_value[key] - if not value or not isinstance(value, list): - continue - for element in value: - path = {key: [element]} - individual_conditions.append(column.contains(path)) + for path in value: + try: + parsed_value = json.loads(path) + except json.JSONDecodeError as error: + raise fastapi.HTTPException( + status_code=http.HTTPStatus.BAD_REQUEST, + detail=f"Invalid JSON in {key}: {error}", + ) + individual_conditions.append(column.contains(parsed_value)) conditions.append(sa.sql.or_(*individual_conditions)) elif key in {"exclude_components_path"}: - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as error: - raise fastapi.HTTPException( - status_code=http.HTTPStatus.BAD_REQUEST, - detail=f"Invalid JSON in {key}: {error}", - ) column_name = "components_json" column = jira_fields_table.columns[column_name] individual_conditions = [] - for key in parsed_value: - value = parsed_value[key] - if not value or not isinstance(value, list): - continue - for element in value: - path = {key: [element]} - individual_conditions.append(column.contains(path)) + for path in value: + try: + parsed_value = json.loads(path) + except json.JSONDecodeError as error: + raise fastapi.HTTPException( + status_code=http.HTTPStatus.BAD_REQUEST, + detail=f"Invalid JSON in {key}: {error}", + ) + individual_conditions.append(column.contains(parsed_value)) conditions.append( sa.sql.not_(sa.sql.or_(*individual_conditions)) ) diff --git a/src/narrativelog/utils.py b/src/narrativelog/utils.py new file mode 100644 index 0000000..32e4d0d --- /dev/null +++ b/src/narrativelog/utils.py @@ -0,0 +1,7 @@ +__all__ = ["JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK"] + +JIRA_OBS_SYSTEMS_HIERARCHY_MARKDOWN_LINK = ( + "[Systems, Sub-Systems and Components Proposal for the OBS Jira project]" + "(https://rubinobs.atlassian.net/wiki/spaces/LSSTCOM/pages/53741849/" + "Systems+Sub-Systems+and+Components+Proposal+for+JIRA)" +)